Skip to main content

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.