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:
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>
);
}
.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
- Theming: map node styles to your CSS.
- Creating an Extension: package a custom node and transform.
- Included Extensions: add more features.
- React and Lexical Extension: integrate custom React UI.