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);

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 these patterns only when a plain ElementNode, framework decorator, or portal would not give enough control.