The CKEditor 5 paragraph feature provides fundamental paragraph editing functionality for CKEditor 5. It implements the core paragraph element support that enables users to create, edit, and format paragraphs within the editor, serving as the foundation for rich text editing.
npm install @ckeditor/ckeditor5-paragraphimport { Paragraph, ParagraphCommand, InsertParagraphCommand, ParagraphButtonUI } from "@ckeditor/ckeditor5-paragraph";For CommonJS (not typical for CKEditor 5):
const { Paragraph, ParagraphCommand, InsertParagraphCommand, ParagraphButtonUI } = require("@ckeditor/ckeditor5-paragraph");import ClassicEditor from "@ckeditor/ckeditor5-editor-classic/src/classiceditor";
import { Paragraph, ParagraphButtonUI } from "@ckeditor/ckeditor5-paragraph";
ClassicEditor
.create(document.querySelector('#editor'), {
plugins: [Paragraph, ParagraphButtonUI],
toolbar: ['paragraph']
})
.then(editor => {
// Use paragraph commands
editor.execute('paragraph');
// Insert new paragraph at current selection
const position = editor.model.document.selection.getFirstPosition();
editor.execute('insertParagraph', { position });
})
.catch(error => {
console.error(error);
});The CKEditor 5 paragraph feature is built around several key components:
The main plugin that enables paragraph support in CKEditor 5 by registering the paragraph element schema, commands, and conversions.
/**
* The paragraph feature for the editor. Introduces the `<paragraph>` element in the model
* which renders as a `<p>` element in the DOM and data.
*/
class Paragraph extends Plugin {
/** Plugin name identifier */
static get pluginName(): 'Paragraph';
/** Indicates this is an official CKEditor 5 plugin */
static get isOfficialPlugin(): true;
/** Set of HTML element names treated as paragraph-like for autoparagraphing */
static paragraphLikeElements: Set<string>;
/** Initializes the plugin, registers schema, commands, and conversions */
init(): void;
}The paragraphLikeElements set contains: 'blockquote', 'dd', 'div', 'dt', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'p', 'td', 'th'.
Usage Example:
import { Paragraph } from "@ckeditor/ckeditor5-paragraph";
// Add to editor plugins
ClassicEditor.create(editorElement, {
plugins: [Paragraph, /* other plugins */]
});
// Check if element is paragraph-like
const isParagraphLike = Paragraph.paragraphLikeElements.has('div'); // trueCommand that converts all blocks in the model selection into paragraphs.
/**
* The paragraph command that converts all blocks in the selection to paragraphs.
*/
class ParagraphCommand extends Command {
/** Creates a new ParagraphCommand instance */
constructor(editor: Editor);
/** Indicates whether the selection start is placed in a paragraph */
readonly value: boolean;
/** Updates command state based on current selection */
refresh(): void;
/**
* Executes the command, converting selected blocks to paragraphs
* @param options - Optional execution options
* @param options.selection - The selection to apply command to (defaults to document selection)
*/
execute(options?: { selection?: ModelSelection | ModelDocumentSelection }): void;
}Usage Example:
// Execute paragraph command on current selection
editor.execute('paragraph');
// Execute on specific selection
const customSelection = editor.model.createSelection(range);
editor.execute('paragraph', { selection: customSelection });
// Check if current selection is in paragraph
const command = editor.commands.get('paragraph');
console.log(command.value); // true if selection is in paragraphCommand that inserts a new paragraph at a specific document position.
/**
* Command that inserts a new paragraph at a specific document position.
* If paragraph is disallowed at the position, attempts to split ancestors to find valid location.
*/
class InsertParagraphCommand extends Command {
/** Creates a new InsertParagraphCommand instance */
constructor(editor: Editor);
/**
* Executes the command to insert a new paragraph
* @param options - Execution options
* @param options.position - Model position where to insert the paragraph
* @param options.attributes - Optional attributes to set on the paragraph
* @returns Position of the inserted paragraph or null if insertion failed
*/
execute(options: {
position: ModelPosition;
attributes?: Record<string, unknown>;
}): ModelPosition | null;
}Usage Example:
import { ModelPosition } from "@ckeditor/ckeditor5-engine";
// Insert paragraph at current selection
const position = editor.model.document.selection.getFirstPosition();
const insertedPosition = editor.execute('insertParagraph', { position });
// Insert paragraph with attributes
const attributedPosition = editor.execute('insertParagraph', {
position,
attributes: { alignment: 'center' }
});
// Insert paragraph before an element
const element = editor.model.document.getRoot().getChild(0);
const beforePosition = editor.model.createPositionBefore(element);
editor.execute('insertParagraph', { position: beforePosition });UI plugin that provides a paragraph button for the toolbar, allowing users to convert selected content to paragraphs.
/**
* UI plugin that defines the 'paragraph' button for the toolbar.
* Must be added manually to plugins - not loaded automatically by Paragraph plugin.
*/
class ParagraphButtonUI extends Plugin {
/** Required plugins - depends on Paragraph plugin */
static get requires(): readonly [typeof Paragraph];
/** Initializes the UI plugin and registers the paragraph button component */
init(): void;
}Usage Example:
import { Paragraph, ParagraphButtonUI } from "@ckeditor/ckeditor5-paragraph";
ClassicEditor.create(editorElement, {
plugins: [Paragraph, ParagraphButtonUI, /* other plugins */],
toolbar: ['paragraph', 'heading1', 'heading2'] // Add paragraph button to toolbar
});// Command Registration Augmentation (from augmentation.ts)
declare module '@ckeditor/ckeditor5-core' {
interface CommandsMap {
insertParagraph: InsertParagraphCommand;
paragraph: ParagraphCommand;
}
interface PluginsMap {
'Paragraph': Paragraph;
}
}
// CKEditor 5 Core Types (imported dependencies)
interface Editor {
commands: CommandsMap;
model: Model;
execute(commandName: string, ...args: any[]): any;
}
interface Plugin {
editor: Editor;
static readonly pluginName?: string;
static readonly requires?: readonly (typeof Plugin)[];
static readonly isOfficialPlugin?: boolean;
init?(): void;
}
interface Command {
editor: Editor;
value?: any;
isEnabled: boolean;
refresh(): void;
execute(...args: any[]): any;
}
// CKEditor 5 Engine Types
interface ModelPosition {
parent: ModelElement;
offset: number;
isAtStart: boolean;
isAtEnd: boolean;
}
interface ModelSelection {
getSelectedBlocks(): IterableIterator<ModelElement>;
getFirstPosition(): ModelPosition | null;
}
interface ModelDocumentSelection extends ModelSelection {}
interface ModelElement {
name: string;
isEmpty: boolean;
parent: ModelElement | ModelDocumentFragment;
is(type: string, name?: string): boolean;
}
interface Model {
document: ModelDocument;
schema: ModelSchema;
canEditAt(selection: ModelSelection | ModelPosition): boolean;
change(callback: (writer: ModelWriter) => void): void;
createPositionBefore(element: ModelElement): ModelPosition;
createPositionAfter(element: ModelElement): ModelPosition;
insertContent(content: ModelElement, position: ModelPosition): void;
}
interface ModelDocument {
selection: ModelDocumentSelection;
}
interface ModelSchema {
register(elementName: string, definition: any): void;
checkChild(parent: ModelElement, child: string): boolean;
findAllowedParent(position: ModelPosition, element: string): ModelElement | null;
setAllowedAttributes(element: ModelElement, attributes: Record<string, unknown>, writer: ModelWriter): void;
isObject(element: ModelElement): boolean;
}
interface ModelWriter {
createElement(name: string): ModelElement;
rename(element: ModelElement, newName: string): void;
setSelection(element: ModelElement, position: 'in' | 'on'): void;
createPositionAt(element: ModelElement, offset: number): ModelPosition;
split(position: ModelPosition, limitElement: ModelElement): { position: ModelPosition };
}When the Paragraph plugin is loaded, it automatically registers these commands:
'paragraph' - ParagraphCommand for converting blocks to paragraphs'insertParagraph' - InsertParagraphCommand for inserting new paragraphsThe plugin registers a 'paragraph' element in the model schema with comprehensive conversion rules:
model.schema.register('paragraph', { inheritAllFrom: '$block' });This registration:
$block: Gets all block-level element properties (can contain text, inline elements)editor.conversion.elementToElement({ model: 'paragraph', view: 'p' });<paragraph> elements render as HTML <p> elements<p> elements convert to model <paragraph> elementsThe plugin includes a special upcast converter for paragraph-like elements:
editor.conversion.for('upcast').elementToElement({
model: (viewElement, { writer }) => {
if (!Paragraph.paragraphLikeElements.has(viewElement.name)) {
return null;
}
if (viewElement.isEmpty) {
return null;
}
return writer.createElement('paragraph');
},
view: /.+/,
converterPriority: 'low'
});This ensures that paragraph-like HTML elements (div, h1-h6, li, td, etc.) are automatically converted to paragraphs when no other plugin handles them, maintaining content structure integrity.
The commands handle various error conditions gracefully:
model.canEditAt() and returns early without executingcheckCanBecomeParagraph() validationExample:
// Command will safely handle non-editable selections
editor.model.change(writer => {
const readOnlySelection = writer.createSelection(readOnlyElement, 'in');
editor.execute('paragraph', { selection: readOnlySelection }); // Safely returns without error
});null if position is not editablenull if no valid position can be found for paragraph insertionExample:
// Command handles invalid positions gracefully
const invalidPosition = editor.model.createPositionAt(objectElement, 0);
const result = editor.execute('insertParagraph', { position: invalidPosition });
if (result === null) {
console.log('Could not insert paragraph at this position');
}