Skip to main content
Commands are Lexical’s primary mechanism for handling user interactions and coordinating behavior between plugins. The command system uses priority-based handlers that can intercept, handle, or pass through commands.

Core Concepts

The command system provides:
  • Decoupled architecture: Plugins can respond to events without tight coupling
  • Priority-based handling: Higher priority handlers run first and can stop propagation
  • Type-safe payloads: Commands carry typed data
  • Implicit update context: Command handlers run in an update context

Creating Commands

Use createCommand() to define a new command:
import { createCommand, LexicalCommand } from 'lexical';

// Command with payload
const INSERT_IMAGE_COMMAND: LexicalCommand<{
  src: string;
  altText: string;
}> = createCommand('INSERT_IMAGE_COMMAND');

// Command without payload
const SAVE_COMMAND: LexicalCommand<void> = createCommand('SAVE_COMMAND');
Commands are identified by reference, not by name. The string argument is for debugging only.

Type-Safe Payloads

The generic type parameter ensures type safety:
type ImagePayload = {
  src: string;
  altText: string;
  width?: number;
  height?: number;
};

const INSERT_IMAGE_COMMAND: LexicalCommand<ImagePayload> = 
  createCommand('INSERT_IMAGE_COMMAND');

// TypeScript enforces payload type
editor.dispatchCommand(INSERT_IMAGE_COMMAND, {
  src: '/image.jpg',
  altText: 'Description',
  width: 500,
});

Registering Command Handlers

Use editor.registerCommand() to handle commands:
import { COMMAND_PRIORITY_NORMAL } from 'lexical';

const removeListener = editor.registerCommand(
  INSERT_IMAGE_COMMAND,
  (payload) => {
    const { src, altText } = payload;
    
    const selection = $getSelection();
    if ($isRangeSelection(selection)) {
      const imageNode = $createImageNode({ src, altText });
      selection.insertNodes([imageNode]);
    }
    
    // Return true to stop propagation
    return true;
  },
  COMMAND_PRIORITY_NORMAL
);

// Clean up when done
removeListener();

Command Priority

Handlers are invoked in priority order (highest to lowest):
import {
  COMMAND_PRIORITY_CRITICAL,  // 4 - Highest
  COMMAND_PRIORITY_HIGH,       // 3
  COMMAND_PRIORITY_NORMAL,     // 2 - Default
  COMMAND_PRIORITY_LOW,        // 1
  COMMAND_PRIORITY_EDITOR,     // 0 - Framework level
} from 'lexical';
Priority Levels:
  • CRITICAL (4): Emergency overrides, rarely used
  • HIGH (3): Important plugins that need early access
  • NORMAL (2): Most plugins
  • LOW (1): Fallback handlers, default behaviors
  • EDITOR (0): Internal framework handlers
Example:
// High priority - runs first
editor.registerCommand(
  FORMAT_TEXT_COMMAND,
  (format) => {
    console.log('High priority handler');
    return false; // Continue propagation
  },
  COMMAND_PRIORITY_HIGH
);

// Normal priority - runs second
editor.registerCommand(
  FORMAT_TEXT_COMMAND,
  (format) => {
    console.log('Normal priority handler');
    return true; // Stop propagation
  },
  COMMAND_PRIORITY_NORMAL
);

// Low priority - never runs (stopped above)
editor.registerCommand(
  FORMAT_TEXT_COMMAND,
  (format) => {
    console.log('Low priority handler');
    return false;
  },
  COMMAND_PRIORITY_LOW
);

Stopping Propagation

Return true to prevent lower-priority handlers from running:
editor.registerCommand(
  MY_COMMAND,
  (payload) => {
    // Handle the command
    
    // Return true = "I handled this, don't run other handlers"
    return true;
    
    // Return false = "Continue to next handler"
    // return false;
  },
  COMMAND_PRIORITY_NORMAL
);

Dispatching Commands

Use editor.dispatchCommand() to trigger a command:
import { FORMAT_TEXT_COMMAND } from 'lexical';

// Dispatch with payload
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');

// Dispatch without payload
editor.dispatchCommand(SAVE_COMMAND, undefined);

// Returns true if any handler stopped propagation
const handled = editor.dispatchCommand(MY_COMMAND, payload);
if (handled) {
  console.log('Command was handled');
}
Commands are dispatched in an implicit editor.update() context, so handlers can use $ functions.

Built-in Commands

Lexical provides many built-in commands:

Text Commands

import {
  FORMAT_TEXT_COMMAND,
  CONTROLLED_TEXT_INSERTION_COMMAND,
  REMOVE_TEXT_COMMAND,
  INSERT_PARAGRAPH_COMMAND,
  INSERT_LINE_BREAK_COMMAND,
} from 'lexical';

// Format selected text
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');

// Insert text
editor.dispatchCommand(CONTROLLED_TEXT_INSERTION_COMMAND, 'Hello');

// Insert paragraph
editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined);

Delete Commands

import {
  DELETE_CHARACTER_COMMAND,
  DELETE_WORD_COMMAND,
  DELETE_LINE_COMMAND,
} from 'lexical';

// true = backward (backspace), false = forward (delete)
editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true);
editor.dispatchCommand(DELETE_WORD_COMMAND, false);

Selection Commands

import {
  SELECTION_CHANGE_COMMAND,
  SELECT_ALL_COMMAND,
} from 'lexical';

editor.registerCommand(
  SELECTION_CHANGE_COMMAND,
  () => {
    const selection = $getSelection();
    // React to selection change
    return false;
  },
  COMMAND_PRIORITY_NORMAL
);

Keyboard Commands

import {
  KEY_DOWN_COMMAND,
  KEY_ENTER_COMMAND,
  KEY_ARROW_LEFT_COMMAND,
  KEY_ARROW_RIGHT_COMMAND,
  KEY_ARROW_UP_COMMAND,
  KEY_ARROW_DOWN_COMMAND,
  KEY_BACKSPACE_COMMAND,
  KEY_DELETE_COMMAND,
  KEY_ESCAPE_COMMAND,
  KEY_TAB_COMMAND,
} from 'lexical';

editor.registerCommand(
  KEY_ENTER_COMMAND,
  (event) => {
    if (event?.shiftKey) {
      // Shift+Enter
      editor.dispatchCommand(INSERT_LINE_BREAK_COMMAND, false);
      return true;
    }
    return false;
  },
  COMMAND_PRIORITY_NORMAL
);

Clipboard Commands

import {
  COPY_COMMAND,
  CUT_COMMAND,
  PASTE_COMMAND,
} from 'lexical';

editor.registerCommand(
  PASTE_COMMAND,
  (event) => {
    // Handle paste
    const clipboardData = event.clipboardData;
    // ...
    return true;
  },
  COMMAND_PRIORITY_HIGH
);

History Commands

import {
  UNDO_COMMAND,
  REDO_COMMAND,
  CAN_UNDO_COMMAND,
  CAN_REDO_COMMAND,
} from 'lexical';

// Trigger undo/redo
editor.dispatchCommand(UNDO_COMMAND, undefined);
editor.dispatchCommand(REDO_COMMAND, undefined);

// Listen to history state
editor.registerCommand(
  CAN_UNDO_COMMAND,
  (canUndo) => {
    setUndoEnabled(canUndo);
    return false;
  },
  COMMAND_PRIORITY_NORMAL
);

Element Formatting

import { FORMAT_ELEMENT_COMMAND } from 'lexical';

// Align elements
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left');
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center');
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right');
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify');

Indentation

import {
  INDENT_CONTENT_COMMAND,
  OUTDENT_CONTENT_COMMAND,
} from 'lexical';

editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined);
editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined);

Command Patterns

Plugin Communication

Plugins communicate via commands:
// Plugin A: Define command
export const HIGHLIGHT_TEXT_COMMAND: LexicalCommand<string> = 
  createCommand('HIGHLIGHT_TEXT_COMMAND');

// Plugin B: Handle command
function HighlightPlugin() {
  const [editor] = useLexicalComposerContext();
  
  useEffect(() => {
    return editor.registerCommand(
      HIGHLIGHT_TEXT_COMMAND,
      (color) => {
        const selection = $getSelection();
        if ($isRangeSelection(selection)) {
          // Apply highlight
        }
        return true;
      },
      COMMAND_PRIORITY_NORMAL
    );
  }, [editor]);
  
  return null;
}

// Dispatch from anywhere
editor.dispatchCommand(HIGHLIGHT_TEXT_COMMAND, 'yellow');

Conditional Handling

editor.registerCommand(
  INSERT_IMAGE_COMMAND,
  (payload) => {
    const selection = $getSelection();
    
    // Only handle if we have a range selection
    if (!$isRangeSelection(selection)) {
      return false; // Let another handler try
    }
    
    // Handle the command
    const imageNode = $createImageNode(payload);
    selection.insertNodes([imageNode]);
    
    return true;
  },
  COMMAND_PRIORITY_NORMAL
);

Fallback Handlers

Use low priority for default behavior:
// High priority - custom handling
editor.registerCommand(
  KEY_ENTER_COMMAND,
  (event) => {
    const selection = $getSelection();
    const node = selection?.getNodes()[0];
    
    if ($isCodeNode(node?.getParent())) {
      // Custom handling in code blocks
      return true;
    }
    
    return false; // Use default
  },
  COMMAND_PRIORITY_HIGH
);

// Low priority - default behavior
editor.registerCommand(
  KEY_ENTER_COMMAND,
  () => {
    // Default paragraph insertion
    editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined);
    return true;
  },
  COMMAND_PRIORITY_LOW
);

Extracting Payload Types

Use CommandPayloadType to extract payload types:
import { CommandPayloadType } from 'lexical';

const MY_COMMAND = createCommand<{ value: number }>();

type MyPayload = CommandPayloadType<typeof MY_COMMAND>;
// = { value: number }

function handleCommand(
  editor: LexicalEditor,
  payload: CommandPayloadType<typeof MY_COMMAND>
) {
  // payload is correctly typed
  console.log(payload.value);
}

React Integration

In React, register commands in useEffect:
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useEffect } from 'react';

function MyPlugin() {
  const [editor] = useLexicalComposerContext();
  
  useEffect(() => {
    return editor.registerCommand(
      MY_COMMAND,
      (payload) => {
        // Handle command
        return true;
      },
      COMMAND_PRIORITY_NORMAL
    );
  }, [editor]);
  
  return null;
}

Best Practices

Even though commands use reference equality, descriptive names help debugging:
// ✅ Good
const INSERT_IMAGE_COMMAND = createCommand('INSERT_IMAGE_COMMAND');

// ❌ Bad
const CMD1 = createCommand('CMD1');
Most commands should use COMMAND_PRIORITY_NORMAL. Use higher priorities only when needed:
// Normal priority for most handlers
COMMAND_PRIORITY_NORMAL

// High priority only when you need to intercept
COMMAND_PRIORITY_HIGH

// Critical priority very rarely
COMMAND_PRIORITY_CRITICAL
Return false to allow other handlers to run:
editor.registerCommand(
  MY_COMMAND,
  (payload) => {
    if (!shouldHandle(payload)) {
      return false; // Let next handler try
    }
    
    handleIt(payload);
    return true; // Handled
  },
  COMMAND_PRIORITY_NORMAL
);
Always call the cleanup function:
const removeCommand = editor.registerCommand(...);

// Later (e.g., in React cleanup)
removeCommand();
Prefer commands over direct function calls between plugins:
// ✅ Good - decoupled
editor.dispatchCommand(MY_COMMAND, payload);

// ❌ Bad - tight coupling
importedPlugin.doSomething(payload);

Type Signatures

type LexicalCommand<TPayload> = {
  type?: string;
};

function createCommand<T>(type?: string): LexicalCommand<T>;

type CommandListener<P> = (
  payload: P,
  editor: LexicalEditor
) => boolean;

type CommandListenerPriority = 0 | 1 | 2 | 3 | 4;

class LexicalEditor {
  registerCommand<P>(
    command: LexicalCommand<P>,
    listener: CommandListener<P>,
    priority: CommandListenerPriority
  ): () => void;
  
  dispatchCommand<TCommand extends LexicalCommand<unknown>>(
    type: TCommand,
    payload: CommandPayloadType<TCommand>
  ): boolean;
}

type CommandPayloadType<TCommand extends LexicalCommand<unknown>> =
  TCommand extends LexicalCommand<infer TPayload> ? TPayload : never;
  • Editor - Registering and dispatching commands
  • Updates - Commands run in update context
  • Selection - Selection-related commands

Build docs developers (and LLMs) love