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);
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);
}
If the node needs to survive copy and paste outside Lexical, implement
exportDOM() and importDOM() in addition to JSON serialization. For example,
a video node may export an iframe or a div with a data attribute that your
DOM import code can recognize later.
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.