Skip to main content

Getting Started with React

Use LexicalExtensionComposer to create an editor from one root extension and manage its lifetime. Extensions provide editing behavior; React components render the editable area and your application's UI.

Creating Basic Rich Text Editor​

Install the packages used below in an existing React application:

npm install lexical @lexical/react @lexical/extension @lexical/rich-text @lexical/history @lexical/clipboard @lexical/html

Keep all Lexical packages on the same version. Define the root extension outside of the component so its identity stays stable across renders. This source is read directly from the runnable rich-text example, including its toolbar, theme, and HTML style extension:

examples/react-rich/src/App.tsx
import {ClipboardDOMImportExtension} from '@lexical/clipboard';
import {
AutoFocusExtension,
EditorStateExtension,
HMRExtension,
} from '@lexical/extension';
import {HistoryExtension} from '@lexical/history';
import {ExtensionComponent} from '@lexical/react/ExtensionComponent';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer';
import {TreeViewExtension} from '@lexical/react/TreeViewExtension';
import {RichTextExtension} from '@lexical/rich-text';
import {configExtension, defineExtension} from 'lexical';

import ExampleTheme from './ExampleTheme';
import {StyleImportExportExtension} from './StyleImportExportExtension';
import Toolbar from './Toolbar';

const placeholder = 'Enter some rich text...';

const isEmbedded = new URLSearchParams(window.location.search).has('embed');

const appExtension = defineExtension({
dependencies: [
configExtension(HMRExtension, {hot: import.meta.hot ?? null}),
RichTextExtension,
ClipboardDOMImportExtension,
StyleImportExportExtension,
EditorStateExtension,
HistoryExtension,
// Let the documentation page keep focus when this example is embedded.
configExtension(AutoFocusExtension, {
disabled: isEmbedded,
}),
TreeViewExtension,
],
name: '@lexical/examples/react-rich',
namespace: 'react-rich',
theme: ExampleTheme,
});

export default function App() {
return (
<LexicalExtensionComposer extension={appExtension} contentEditable={null}>
<div className="editor-container">
<Toolbar />
<div className="editor-inner">
<ContentEditable
className="editor-input"
aria-label="Rich text editor"
aria-placeholder={placeholder}
placeholder={
<div className="editor-placeholder">{placeholder}</div>
}
/>
<ExtensionComponent lexical:extension={TreeViewExtension} />
</div>
</div>
</LexicalExtensionComposer>
);
}
examples/react-rich/src/styles.css
.editor-container {
margin: 20px auto 20px auto;
border-radius: 2px;
max-width: 600px;
color: #000;
position: relative;
line-height: 20px;
font-weight: 400;
text-align: left;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}

.editor-inner {
background: #fff;
position: relative;
}

.editor-input {
min-height: 150px;
resize: none;
font-size: 16px;
caret-color: rgb(5, 5, 5);
position: relative;
tab-size: 1;
outline: 0;
padding: 15px 10px;
caret-color: #444;
}

.editor-placeholder {
color: #999;
overflow: hidden;
position: absolute;
text-overflow: ellipsis;
top: 15px;
left: 10px;
font-size: 16px;
user-select: none;
display: inline-block;
pointer-events: none;
}

.editor-text-bold {
font-weight: bold;
}

.editor-text-italic {
font-style: italic;
}

Use Open in StackBlitz in the rich-text example below for the complete project, including the imported local modules and the rest of the stylesheet.

The composer renders a ContentEditable by default. Here contentEditable={null} lets us place it inside our own layout. The composer also sets up React decorator rendering and its error boundary, and disposes the editor when it unmounts.

RichTextExtension registers its nodes and editing behavior, HistoryExtension provides undo and redo, and AutoFocusExtension focuses the editor when it mounts. The examples opt out of autofocus when their URL includes ?embed, which the documentation embeds add to avoid taking focus from the page. Standalone examples and StackBlitz previews keep autofocus enabled, even inside an iframe. HMRExtension preserves editor state and history during Vite updates; React's composer manages editor disposal. Passing hot: null is a no-op when HMR is unavailable. The composer accepts a single root extension, so this example includes HMR in its dependencies. Omit it for other bundlers; TypeScript projects using Vite need vite/client types, as described in Quick Start. Omit autofocus if focus should stay elsewhere on your page. ClipboardDOMImportExtension enables the extensions' HTML import rules for paste.

There is no separate nodes list or set of behavior plugins to keep in sync with these dependencies. Errors throw by default; add onError to the root extension only when you need custom handling.

Changing the extension reference recreates the editor. Keep it at module scope, and choose the feature set when creating the editor. Features that support runtime changes expose signals.

Plain text​

For a plain-text editor, install @lexical/plain-text and replace RichTextExtension with PlainTextExtension from that package. Remove ClipboardDOMImportExtension, since plain-text paste does not import HTML. Keep history, autofocus, and the same React composition. The plain-text example omits the rich-text toolbar and StyleImportExportExtension as well.

Adding UI to control text formatting​

Lexical provides commands and state for your UI. A React component inside the composer can access the editor with useLexicalComposerContext:

import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {FORMAT_TEXT_COMMAND} from 'lexical';

function BoldButton() {
const [editor] = useLexicalComposerContext();
return (
<button
type="button"
onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')}>
Bold
</button>
);
}

Render <BoldButton /> inside the composer alongside your editable area. RichTextExtension handles the command. UI components do not need to register the editing behavior again.

For reactive UI, use an extension's output signals. For example, useExtensionSignalValue(HistoryExtension, 'canUndo') tells an Undo button when it can be enabled. TreeViewExtension provides a debug panel through ExtensionComponent:

import {ExtensionComponent} from '@lexical/react/ExtensionComponent';
import {TreeViewExtension} from '@lexical/react/TreeViewExtension';

// Render inside the composer (TreeViewExtension is already a dependency above):
<ExtensionComponent lexical:extension={TreeViewExtension} />;

The runnable example includes a toolbar, the debug panel, and an extension that customizes HTML style import and export. Its theme classes are defined in ExampleTheme.ts and styled in styles.css.

Saving Lexical State​

For an explicit Save action, read and serialize the editor when the user clicks. Use editor.read('latest', ...) to read the latest committed state without forcing a commit. toJSON() produces a JSON-compatible object, and JSON.stringify() produces the string you can store:

import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';

function SaveButton({onSave}) {
const [editor] = useLexicalComposerContext();
return (
<button
type="button"
onClick={() => {
const json = editor.read('latest', () =>
JSON.stringify(editor.getEditorState().toJSON()),
);
onSave(json);
}}>
Save
</button>
);
}

Render this inside the composer and pass your persistence callback as onSave. For autosave or other subscriptions, register an update listener in an extension and return its cleanup function, as shown in the vanilla guide. For React UI that needs the current state, add EditorStateExtension from @lexical/extension and subscribe with useSignalValue(useExtensionDependency(EditorStateExtension).output).

Initial content​

Use $initialEditorState on the root extension for either serialized JSON or a synchronous initialization function:

import {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';

function $initialEditorState() {
$getRoot().append(
$createParagraphNode().append($createTextNode('Hello world')),
);
}

Set $initialEditorState to this function on appExtension. It runs during editor initialization. Lexical owns subsequent edits; do not feed every change back into editor.setEditorState() as if the editor were a controlled input. For deliberately loading a different document, see Editor State.

Next steps​