Lexical Editor Integration in CMS
Lexical — an editor developed by Meta with emphasis on extensibility and performance. Written in TypeScript, developed using experience from building Facebook's editor. Headless like Tiptap — all UI is built by the developer.
Installation
npm install lexical @lexical/react @lexical/rich-text @lexical/list @lexical/link
npm install @lexical/markdown @lexical/code @lexical/utils @lexical/selection
Basic Configuration
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
import { ListNode, ListItemNode } from '@lexical/list';
import { LinkNode, AutoLinkNode } from '@lexical/link';
import { CodeNode, CodeHighlightNode } from '@lexical/code';
const editorConfig = {
namespace: 'cms-editor',
theme: {
paragraph: 'mb-3',
heading: { h1: 'text-3xl font-bold', h2: 'text-2xl font-bold', h3: 'text-xl font-bold' },
list: { ol: 'list-decimal ml-6', ul: 'list-disc ml-6' },
text: { bold: 'font-bold', italic: 'italic', underline: 'underline' }
},
nodes: [HeadingNode, QuoteNode, ListNode, ListItemNode, LinkNode, AutoLinkNode, CodeNode, CodeHighlightNode],
onError: console.error
};
function LexicalEditor({ initialState, onChange }) {
return (
<LexicalComposer initialConfig={{ ...editorConfig, editorState: initialState }}>
<div className="relative border rounded-lg">
<ToolbarPlugin />
<div className="relative">
<RichTextPlugin
contentEditable={<ContentEditable className="outline-none p-4 min-h-96" />}
placeholder={<div className="absolute top-4 left-4 text-gray-400">Start typing...</div>}
ErrorBoundary={({ children }) => children}
/>
<HistoryPlugin />
<AutoFocusPlugin />
<ListPlugin />
<LinkPlugin />
<OnChangePlugin onChange={(editorState) => {
editorState.read(() => {
const json = editorState.toJSON();
onChange(JSON.stringify(json));
});
}} />
</div>
</div>
</LexicalComposer>
);
}
Custom Toolbar
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { FORMAT_TEXT_COMMAND } from 'lexical';
import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND } from '@lexical/list';
function ToolbarPlugin() {
const [editor] = useLexicalComposerContext();
const [isBold, setIsBold] = useState(false);
useEffect(() => {
return editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
setIsBold(selection.hasFormat('bold'));
}
});
});
}, [editor]);
return (
<div className="flex gap-1 p-2 border-b">
<button
onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')}
className={isBold ? 'bg-gray-200 rounded px-2' : 'px-2'}
>
B
</button>
<button onClick={() => editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined)}>
List
</button>
</div>
);
}
Custom Nodes
import { DecoratorNode, SerializedLexicalNode } from 'lexical';
class ImageNode extends DecoratorNode<React.ReactElement> {
static getType() { return 'image'; }
static clone(node: ImageNode) { return new ImageNode(node.__src, node.__alt, node.__key); }
constructor(private __src: string, private __alt: string, key?: string) {
super(key);
}
createDOM() {
return document.createElement('div');
}
decorate() {
return <img src={this.__src} alt={this.__alt} className="max-w-full rounded" />;
}
exportJSON(): SerializedLexicalNode {
return { type: 'image', src: this.__src, alt: this.__alt, version: 1 };
}
}
Saving and Loading State
// Saving
const jsonState = JSON.stringify(editor.getEditorState().toJSON());
// Loading
editor.update(() => {
const state = editor.parseEditorState(savedJsonState);
editor.setEditorState(state);
});
Integration timeline: 2–4 days for a full-featured editor with toolbar, custom nodes, and media loading.







