Skip to main content
Custom nodes allow you to extend Lexical’s functionality by creating your own node types with custom behavior, rendering, and serialization.

Node Base Classes

Lexical provides three base classes you can extend:
  • TextNode - For inline text content with formatting
  • ElementNode - For block-level or container elements
  • DecoratorNode - For React components or custom DOM rendering

Creating a Custom TextNode

Extend TextNode when you need custom inline text with additional properties:
import { TextNode, SerializedTextNode, Spread, EditorConfig } from 'lexical';

export type SerializedEmojiNode = Spread<
  {
    className: string;
  },
  SerializedTextNode
>;

export class EmojiNode extends TextNode {
  __className: string;

  static getType(): string {
    return 'emoji';
  }

  static clone(node: EmojiNode): EmojiNode {
    return new EmojiNode(node.__className, node.__text, node.__key);
  }

  constructor(className: string, text: string, key?: NodeKey) {
    super(text, key);
    this.__className = className;
  }

  createDOM(config: EditorConfig): HTMLElement {
    const dom = document.createElement('span');
    const inner = super.createDOM(config);
    dom.className = this.__className;
    inner.className = 'emoji-inner';
    dom.appendChild(inner);
    return dom;
  }

  updateDOM(
    prevNode: this,
    dom: HTMLElement,
    config: EditorConfig
  ): boolean {
    const inner = dom.firstChild;
    if (inner === null) {
      return true;
    }
    super.updateDOM(prevNode, inner as HTMLElement, config);
    return false;
  }

  static importJSON(serializedNode: SerializedEmojiNode): EmojiNode {
    return $createEmojiNode(
      serializedNode.className,
      serializedNode.text,
    ).updateFromJSON(serializedNode);
  }

  exportJSON(): SerializedEmojiNode {
    return {
      ...super.exportJSON(),
      className: this.getClassName(),
    };
  }

  getClassName(): string {
    return this.getLatest().__className;
  }
}

export function $createEmojiNode(
  className: string,
  emojiText: string,
): EmojiNode {
  return $applyNodeReplacement(
    new EmojiNode(className, emojiText).setMode('token')
  );
}
See packages/lexical-playground/src/nodes/EmojiNode.tsx for the complete implementation.

Creating a Custom ElementNode

Extend ElementNode for block-level or container elements:
import {
  ElementNode,
  EditorConfig,
  LexicalNode,
  SerializedElementNode,
  DOMConversionMap,
  LexicalEditor,
} from 'lexical';
import { addClassNamesToElement } from '@lexical/utils';

export class QuoteNode extends ElementNode {
  static getType(): string {
    return 'quote';
  }

  static clone(node: QuoteNode): QuoteNode {
    return new QuoteNode(node.__key);
  }

  createDOM(config: EditorConfig): HTMLElement {
    const element = document.createElement('blockquote');
    addClassNamesToElement(element, config.theme.quote);
    return element;
  }

  updateDOM(prevNode: this, dom: HTMLElement): boolean {
    return false;
  }

  static importDOM(): DOMConversionMap | null {
    return {
      blockquote: (node: Node) => ({
        conversion: $convertBlockquoteElement,
        priority: 0,
      }),
    };
  }

  static importJSON(serializedNode: SerializedQuoteNode): QuoteNode {
    return $createQuoteNode().updateFromJSON(serializedNode);
  }

  exportJSON(): SerializedQuoteNode {
    return {
      ...super.exportJSON(),
    };
  }

  insertNewAfter(_: RangeSelection): ParagraphNode {
    const newBlock = $createParagraphNode();
    const direction = this.getDirection();
    newBlock.setDirection(direction);
    this.insertAfter(newBlock);
    return newBlock;
  }

  collapseAtStart(): true {
    const paragraph = $createParagraphNode();
    const children = this.getChildren();
    children.forEach((child) => paragraph.append(child));
    this.replace(paragraph);
    return true;
  }
}

export function $createQuoteNode(): QuoteNode {
  return $applyNodeReplacement(new QuoteNode());
}
See packages/lexical-rich-text/src/index.ts for the complete QuoteNode implementation.

Creating a Custom DecoratorNode

Extend DecoratorNode when you need to render a React component:
import { DecoratorNode, EditorConfig, LexicalEditor, NodeKey } from 'lexical';

export class ImageNode extends DecoratorNode<JSX.Element> {
  __src: string;
  __altText: string;
  __width: 'inherit' | number;
  __height: 'inherit' | number;

  static getType(): string {
    return 'image';
  }

  static clone(node: ImageNode): ImageNode {
    return new ImageNode(
      node.__src,
      node.__altText,
      node.__width,
      node.__height,
      node.__key,
    );
  }

  constructor(
    src: string,
    altText: string,
    width?: 'inherit' | number,
    height?: 'inherit' | number,
    key?: NodeKey,
  ) {
    super(key);
    this.__src = src;
    this.__altText = altText;
    this.__width = width || 'inherit';
    this.__height = height || 'inherit';
  }

  createDOM(config: EditorConfig): HTMLElement {
    const span = document.createElement('span');
    const theme = config.theme;
    const className = theme.image;
    if (className !== undefined) {
      span.className = className;
    }
    return span;
  }

  updateDOM(): false {
    return false;
  }

  decorate(): JSX.Element {
    return (
      <ImageComponent
        src={this.__src}
        altText={this.__altText}
        width={this.__width}
        height={this.__height}
        nodeKey={this.getKey()}
      />
    );
  }
}

Required Methods

All custom nodes must implement:
1
Static Methods
2
  • getType() - Returns a unique string identifier
  • clone() - Creates a copy of the node
  • importJSON() - Deserializes from JSON
  • 3
    Instance Methods
    4
  • createDOM() - Creates the DOM element
  • updateDOM() - Updates DOM when node changes (return true to recreate)
  • exportJSON() - Serializes to JSON
  • Registering Custom Nodes

    Register your custom nodes in the editor configuration:
    import { createEditor } from 'lexical';
    import { EmojiNode } from './nodes/EmojiNode';
    import { QuoteNode } from './nodes/QuoteNode';
    
    const editor = createEditor({
      namespace: 'MyEditor',
      nodes: [
        EmojiNode,
        QuoteNode,
        // other custom nodes
      ],
      onError: (error) => {
        console.error(error);
      },
    });
    
    In React:
    import { LexicalComposer } from '@lexical/react/LexicalComposer';
    import { EmojiNode } from './nodes/EmojiNode';
    
    function Editor() {
      const initialConfig = {
        namespace: 'MyEditor',
        nodes: [EmojiNode],
        onError: (error: Error) => {
          console.error(error);
        },
      };
    
      return (
        <LexicalComposer initialConfig={initialConfig}>
          {/* editor components */}
        </LexicalComposer>
      );
    }
    

    Best Practices

    • Immutability: Always use getWritable() before modifying node properties
    • Factory Functions: Export a $createYourNode() function (follows $ convention)
    • Type Guards: Export a $isYourNode() type guard function
    • Serialization: Ensure all custom properties are serialized in exportJSON()
    • DOM Conversion: Implement importDOM() for paste/HTML support
    • Property Access: Use getLatest() when reading properties to get the current version

    Node Lifecycle

    1. Construction: Node is created with new YourNode() or factory function
    2. Registration: Node class is registered in editor config
    3. Reconciliation: createDOM() is called during first render
    4. Updates: updateDOM() is called when node changes
    5. Cloning: clone() is called when node is copied or made writable
    6. Serialization: exportJSON() is called for persistence
    7. Deserialization: importJSON() is called when loading saved content

    See Also

    Build docs developers (and LLMs) love