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
);
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
);
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
Use descriptive command names
Even though commands use reference equality, descriptive names help debugging: // ✅ Good
const INSERT_IMAGE_COMMAND = createCommand ( 'INSERT_IMAGE_COMMAND' );
// ❌ Bad
const CMD1 = createCommand ( 'CMD1' );
Choose appropriate priorities
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 when passing through
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 ();
Use commands for plugin communication
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