Decorators
Decorator nodes are used when a piece of editor content needs a custom view instead of a text-only or element-only DOM representation. Common examples are media embeds, mentions, cards, horizontal rules, or interactive controls that need application UI inside the editor.
A DecoratorNode still belongs to the EditorState, but its visible UI comes
from decorate(). The DOM element returned by createDOM() is the host that
Lexical reconciles. The decorated value returned by decorate() is rendered
into that host by the framework integration, such as React's decorators in
@lexical/react.
When to use a decorator
Use a decorator node when:
- The content is represented by structured data rather than editable text.
- The rendered UI is owned by your application or framework.
- Selection should treat the content as a single node or custom interactive region.
- The node needs a separate DOM import, DOM export, or JSON serialization shape.
Prefer TextNode or ElementNode when the content can be edited directly as
normal text or nested editor content. For example, a custom paragraph style is
usually an ElementNode, while a video embed is a decorator.
Inline and block decorators
DecoratorNode is inline by default. Override isInline() to return false
for block-level content such as horizontal rules, video embeds, or cards.
class VideoNode extends DecoratorNode<ReactNode> {
isInline(): false {
return false;
}
createDOM(): HTMLElement {
return document.createElement('div');
}
updateDOM(): false {
return false;
}
decorate(): ReactNode {
return <VideoPlayer />;
}
}
Block decorators often also need custom selection behavior. The built-in
HorizontalRuleNode is a useful reference because it is a block decorator that
registers click handling and node selection support.
Registering decorators
As with other custom nodes, a decorator node must be registered before it is used. In React, pass the node class in the editor config:
<LexicalComposer
initialConfig={{
namespace: 'Example',
nodes: [VideoNode],
onError(error) {
throw error;
},
}}>
{/* editor UI */}
</LexicalComposer>
A common pattern is to pair the node with a plugin or extension that registers commands for inserting it.
export const INSERT_VIDEO_COMMAND: LexicalCommand<string> = createCommand(
'INSERT_VIDEO_COMMAND',
);
function VideoPlugin(): null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
return editor.registerCommand(
INSERT_VIDEO_COMMAND,
videoID => {
editor.update(() => {
$insertNodes([$createVideoNode(videoID)]);
});
return true;
},
COMMAND_PRIORITY_EDITOR,
);
}, [editor]);
return null;
}
The command can then be dispatched from a toolbar, menu, or any other UI:
editor.dispatchCommand(INSERT_VIDEO_COMMAND, videoID);
For newer code, prefer putting the node registration and the behavior that
goes with it in an extension instead of spreading it
across an editor config and separate plugins. React applications can mount that
root extension with
LexicalExtensionComposer.
This keeps node registration, commands, transforms, mutation listeners, and
framework decorators in one composable unit.
State and serialization
Store only serializable data on the node or in NodeState.
The decorated UI should be derived from that data. Do not store React elements,
DOM nodes, functions, Map, Set, or other non-serializable values in node
state.
The example below stores a video id with NodeState and renders a React component from that id:
const videoIDState = createState('videoID', {
parse: value => (typeof value === 'string' ? value : ''),
});
class VideoNode extends DecoratorNode<ReactNode> {
$config() {
return this.config('video', {
extends: DecoratorNode,
stateConfigs: [{flat: true, stateConfig: videoIDState}],
});
}
createDOM(): HTMLElement {
return document.createElement('div');
}
updateDOM(): false {
return false;
}
decorate(): ReactNode {
return <VideoPlayer videoID={$getState(this, videoIDState)} />;
}
}
export function $createVideoNode(videoID: string): VideoNode {
return $setState($create(VideoNode), videoIDState, videoID);
}
NodeState can hold richer serializable shapes as long as the state config
knows how to parse them. Use a
StateValueConfig when a
decorator needs structured data such as ids, dimensions, captions, or embed
metadata.
If the node needs to survive copy and paste outside Lexical, implement
exportDOM() in addition to JSON serialization. For importing HTML, prefer
DOMImportExtension for new code. Static
importDOM() still works, but it shares the same import path as the extension
pipeline and is expected to be deprecated eventually, so avoid mixing both
approaches for the same import behavior.
Rendering model
createDOM() should create the stable host element for the node. updateDOM()
returns whether Lexical should replace that host when the node changes. Most
decorator nodes return false because the host can stay in place while the
decorated value changes.
decorate() returns the framework-specific rendered value. In React, the
React plugin renders this value with a portal into the host element created by
createDOM(). Non-React integrations can use the same node data and provide
their own rendering layer.
Keep editor data and UI state separate. The node should store the content that belongs in the editor state, while transient UI details such as hover state, loading state, or local component state should live inside the decorated component.
Decorators do not have to be tied to a UI framework. A DecoratorNode can
return null from decorate() and do its visible work in createDOM() and
updateDOM(), or delegate behavior to extension hooks such as
DOMRenderExtension or a mutation listener.
HorizontalRuleNode from @lexical/extension is an example of a decorator
node whose behavior can be managed without rendering a React component from
decorate().
Decorator nodes also combine well with
named slots when surrounding
application UI needs to be mounted in predictable places around the editor. For
advanced DOM ownership, DOMSlot and ElementNode patterns let an extension
place arbitrary DOM inside or around lexical content while still letting
Lexical manage the actual editor children. Use those patterns when the content
inside the custom UI should remain editable by Lexical. The playground's
ReviewExtension and ReviewNode on main are a useful example: the node uses
getDOMSlot() for its editable body, while the extension tracks live review
nodes with a mutation listener and renders the surrounding review UI. Use these
patterns only when a plain ElementNode, framework decorator, or portal would
not give enough control.