Skip to main content
The LexicalEditor is the central orchestrator of Lexical. It manages the editor state, configuration, listeners, commands, and DOM reconciliation. Every Lexical editor begins with a call to createEditor().

Creating an Editor

The createEditor() function is the primary way to initialize a Lexical editor:
import { createEditor } from 'lexical';

const editor = createEditor({
  namespace: 'MyEditor',
  theme: {
    // Theme classes for styling
    paragraph: 'editor-paragraph',
    text: {
      bold: 'editor-text-bold',
      italic: 'editor-text-italic',
    },
  },
  nodes: [
    // Custom node classes
    HeadingNode,
    QuoteNode,
  ],
  onError: (error) => {
    console.error(error);
  },
});

Configuration Options

The CreateEditorArgs interface defines all available configuration options:
type CreateEditorArgs = {
  /** Disable event listeners (useful for headless environments) */
  disableEvents?: boolean;
  
  /** Initial editor state to load */
  editorState?: EditorState;
  
  /** Unique namespace for the editor (defaults to auto-generated) */
  namespace?: string;
  
  /** Array of node classes to register */
  nodes?: ReadonlyArray<LexicalNodeConfig>;
  
  /** Error handler callback */
  onError?: ErrorHandler;
  
  /** Parent editor for nested editor scenarios */
  parentEditor?: LexicalEditor;
  
  /** Whether the editor is editable (default: true) */
  editable?: boolean;
  
  /** Theme configuration for applying CSS classes */
  theme?: EditorThemeClasses;
  
  /** HTML import/export configuration */
  html?: HTMLConfig;
};

Attaching to the DOM

After creating the editor, attach it to a DOM element:
const contentEditableElement = document.getElementById('editor');
editor.setRootElement(contentEditableElement);
The root element should have the contenteditable attribute. Lexical will manage all DOM interactions through this element.

Core Editor Methods

State Management

getEditorState()

Returns the current immutable EditorState:
const currentState = editor.getEditorState();

setEditorState()

Imperatively sets a new editor state:
const newState = editor.parseEditorState(jsonState);
editor.setEditorState(newState);
Direct state mutation via setEditorState() triggers reconciliation. For programmatic changes, prefer using editor.update().

update()

The primary way to make changes to the editor state:
editor.update(() => {
  const root = $getRoot();
  const paragraph = $createParagraphNode();
  const text = $createTextNode('Hello, world!');
  paragraph.append(text);
  root.append(paragraph);
});
See Updates for more details.

read()

Read from the editor state without making changes:
const textContent = editor.read(() => {
  const root = $getRoot();
  return root.getTextContent();
});

Listeners

registerUpdateListener()

Listen to all editor updates:
const removeListener = editor.registerUpdateListener(
  ({ editorState, dirtyElements, dirtyLeaves, tags }) => {
    // React to changes
    console.log('Editor updated');
    
    editorState.read(() => {
      const root = $getRoot();
      console.log(root.getTextContent());
    });
  }
);

// Cleanup
removeListener();
The UpdateListenerPayload includes:
interface UpdateListenerPayload {
  /** Map of dirty ElementNode keys to intentional mutation flags */
  dirtyElements: Map<NodeKey, boolean>;
  
  /** Set of dirty leaf node keys */
  dirtyLeaves: Set<NodeKey>;
  
  /** New editor state after update */
  editorState: EditorState;
  
  /** Map of node mutations (created/updated/destroyed) */
  mutatedNodes: null | MutatedNodes;
  
  /** Set of normalized (merged) TextNode keys */
  normalizedNodes: Set<NodeKey>;
  
  /** Previous editor state */
  prevEditorState: EditorState;
  
  /** Tags associated with this update */
  tags: Set<string>;
}

registerMutationListener()

Listen to specific node type mutations:
const removeMutationListener = editor.registerMutationListener(
  ImageNode,
  (mutatedNodes, { updateTags }) => {
    mutatedNodes.forEach((mutation, nodeKey) => {
      if (mutation === 'created') {
        const element = editor.getElementByKey(nodeKey);
        // Attach event listeners to the DOM element
      }
    });
  }
);
By default (as of v0.21.0), mutation listeners are called immediately with existing nodes. Use skipInitialization: true to opt out of this behavior.

registerCommand()

Register command handlers with priority levels:
import { FORMAT_TEXT_COMMAND, COMMAND_PRIORITY_NORMAL } from 'lexical';

const removeCommand = editor.registerCommand(
  FORMAT_TEXT_COMMAND,
  (payload) => {
    // Handle the command
    console.log('Format command:', payload);
    
    // Return true to stop propagation
    return false;
  },
  COMMAND_PRIORITY_NORMAL
);
See Commands for detailed command system documentation.

Other Listeners

// Editable state changes
editor.registerEditableListener((isEditable) => {
  console.log('Editable:', isEditable);
});

// Root element changes
editor.registerRootListener((rootElement, prevRootElement) => {
  if (rootElement) {
    // Editor mounted
  }
});

// Text content changes (debounced)
editor.registerTextContentListener((text) => {
  console.log('Text:', text);
});

// Decorator changes (for DecoratorNodes)
editor.registerDecoratorListener((decorators) => {
  // Render decorators in your framework
});

Commands

dispatchCommand()

Dispatch a command to all registered handlers:
import { FORMAT_TEXT_COMMAND } from 'lexical';

editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
Commands are dispatched in priority order (highest first), and handlers can stop propagation by returning true.

Focus Management

focus()

Focus the editor programmatically:
editor.focus(() => {
  // Optional callback after focus
}, {
  defaultSelection: 'rootEnd' // or 'rootStart'
});

blur()

Remove focus from the editor:
editor.blur();

Node Registration

hasNode() / hasNodes()

Check if node types are registered:
if (editor.hasNode(HeadingNode)) {
  // HeadingNode is available
}

if (editor.hasNodes([HeadingNode, QuoteNode])) {
  // Both nodes are available
}

Serialization

toJSON()

Serialize the entire editor to JSON:
const json = editor.toJSON();
// { editorState: { root: { ... } } }

parseEditorState()

Parse JSON back into an EditorState:
const state = editor.parseEditorState(jsonString);
editor.setEditorState(state);

Editor State

Editable Mode

// Check if editable
if (editor.isEditable()) {
  // Editor accepts user input
}

// Toggle editable
editor.setEditable(false); // Read-only
editor.setEditable(true);  // Editable

Composition State

Check if the editor is in composition mode (IME input):
if (editor.isComposing()) {
  // User is composing text via IME
}

Advanced Features

Node Transforms

Register transforms that run when nodes of a specific type are marked dirty:
const removeTransform = editor.registerNodeTransform(
  TextNode,
  (node) => {
    // Transform logic
    if (node.getTextContent().includes('TODO:')) {
      node.setFormat('bold');
    }
  }
);
See Transforms for more details.

Getting DOM Elements

getElementByKey()

Get the DOM element for a node key:
const element = editor.getElementByKey(nodeKey);
if (element) {
  // Attach event listeners, etc.
}

getRootElement()

Get the current root contenteditable element:
const rootElement = editor.getRootElement();

Decorators

Get all current decorators (for DecoratorNode rendering):
const decorators = editor.getDecorators();

Best Practices

Never access node properties outside of an update() or read() callback. The editor state is only accessible within these contexts.
// ❌ Bad
const root = $getRoot(); // Will throw error

// ✅ Good
editor.read(() => {
  const root = $getRoot();
});
Always store and call the cleanup functions returned by register* methods:
const cleanup = editor.registerUpdateListener(listener);

// Later...
cleanup();
Understand the command priority system:
  • COMMAND_PRIORITY_EDITOR (0) - Framework/editor level
  • COMMAND_PRIORITY_LOW (1) - Low priority plugins
  • COMMAND_PRIORITY_NORMAL (2) - Most plugins
  • COMMAND_PRIORITY_HIGH (3) - Important plugins
  • COMMAND_PRIORITY_CRITICAL (4) - Critical overrides
Higher priorities run first and can intercept commands.
Multiple synchronous update() calls are batched together:
editor.update(() => { /* change 1 */ });
editor.update(() => { /* change 2 */ });
// Both changes reconciled in a single pass

Type Signatures

LexicalEditor Class

Key properties (internal, for reference):
class LexicalEditor {
  /** Current editor state */
  _editorState: EditorState;
  
  /** Editor configuration */
  _config: EditorConfig;
  
  /** Registered node types */
  _nodes: RegisteredNodes;
  
  /** Root DOM element */
  _rootElement: null | HTMLElement;
  
  /** Whether editor is editable */
  _editable: boolean;
  
  // ... many other internal properties
}
  • Editor State - Understanding the immutable state model
  • Updates - How to make changes to the editor
  • Commands - The command system in depth
  • Nodes - Working with the node system

Build docs developers (and LLMs) love