Skip to main content

Overview

In Lexical’s React integration, plugins are components that hook into the editor lifecycle to add functionality. They typically return null but register listeners, commands, and transforms during their mount phase.

Plugin Architecture

Plugins follow React’s component lifecycle:
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useEffect } from 'react';

function MyPlugin() {
  const [editor] = useLexicalComposerContext();
  
  useEffect(() => {
    // Setup: register listeners, commands, transforms
    const unregister = editor.registerUpdateListener(({ editorState }) => {
      // React to editor updates
    });
    
    // Cleanup: automatically called on unmount
    return unregister;
  }, [editor]);
  
  return null; // Plugins typically don't render anything
}

Core Plugins

RichTextPlugin

Enables rich text editing with support for headings, lists, formatting, and more.
contentEditable
JSX.Element
required
The ContentEditable component where users type.
placeholder
JSX.Element | ((isEditable: boolean) => JSX.Element)
Placeholder content shown when the editor is empty.
ErrorBoundary
ErrorBoundaryType
required
Error boundary component for handling rendering errors.
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';

function Editor() {
  return (
    <RichTextPlugin
      contentEditable={
        <ContentEditable 
          className="editor-input"
          aria-placeholder="Enter text..."
          placeholder={
            <div className="editor-placeholder">Enter text...</div>
          }
        />
      }
      ErrorBoundary={LexicalErrorBoundary}
    />
  );
}

PlainTextPlugin

Enables plain text editing without rich text features.
import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';

function Editor() {
  return (
    <PlainTextPlugin
      contentEditable={<ContentEditable className="editor-input" />}
      placeholder={<div>Enter plain text...</div>}
      ErrorBoundary={LexicalErrorBoundary}
    />
  );
}

HistoryPlugin

Adds undo/redo functionality.
delay
number
default:"1000"
Delay in milliseconds before creating a new history entry.
externalHistoryState
HistoryState
External history state for synchronizing undo/redo across multiple editors.
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';

<HistoryPlugin delay={1000} />

OnChangePlugin

Executes a callback when the editor state changes.
onChange
(editorState: EditorState, editor: LexicalEditor, tags: Set<string>) => void
required
Callback invoked on editor state changes.
ignoreHistoryMergeTagChange
boolean
default:"true"
Whether to ignore changes tagged with HISTORY_MERGE_TAG.
ignoreSelectionChange
boolean
default:"false"
Whether to ignore changes that only affect selection.
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';

function Editor() {
  const handleChange = (editorState, editor, tags) => {
    editorState.read(() => {
      const json = editorState.toJSON();
      // Save to database, localStorage, etc.
    });
  };

  return <OnChangePlugin onChange={handleChange} />;
}

AutoFocusPlugin

Automatically focuses the editor on mount.
defaultSelection
'rootStart' | 'rootEnd'
Where to place the cursor when focusing.
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';

<AutoFocusPlugin defaultSelection="rootStart" />

Feature Plugins

ListPlugin

Enables ordered lists, unordered lists, and checklists.
hasStrictIndent
boolean
default:"false"
Enforces strict indentation rules for list items.
shouldPreserveNumbering
boolean
default:"false"
Preserves numbering continuity when splitting numbered lists.
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
import { ListNode, ListItemNode } from '@lexical/list';

// Add to initialConfig.nodes:
// nodes: [ListNode, ListItemNode]

<ListPlugin hasStrictIndent shouldPreserveNumbering />

LinkPlugin

Enables link functionality.
import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
import { LinkNode } from '@lexical/link';

// Add to initialConfig.nodes:
// nodes: [LinkNode]

<LinkPlugin />

AutoLinkPlugin

Automatically converts URLs to links as you type.
matchers
Array<LinkMatcher>
required
Array of matchers that define URL patterns to auto-link.
onChange
ChangeHandler
Callback invoked when a link is created or modified.
import { AutoLinkPlugin, createLinkMatcherWithRegExp } from '@lexical/react/LexicalAutoLinkPlugin';
import { AutoLinkNode } from '@lexical/link';

// Add to initialConfig.nodes:
// nodes: [AutoLinkNode]

const URL_MATCHER =
  /((https?:\/\/(www\.)?)|((www\.)))[\w\W]+[\/\w\W-_~?&=%.]+/i;

const MATCHERS = [
  createLinkMatcherWithRegExp(URL_MATCHER, (text) => {
    return text.startsWith('http') ? text : `https://${text}`;
  }),
];

<AutoLinkPlugin matchers={MATCHERS} />

MarkdownShortcutPlugin

Enables Markdown shortcuts (e.g., **bold**, # Heading).
import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin';
import { TRANSFORMERS } from '@lexical/markdown';

<MarkdownShortcutPlugin transformers={TRANSFORMERS} />

CharacterLimitPlugin

Limits the number of characters in the editor.
maxLength
number
required
Maximum number of characters allowed.
renderer
(props: { characters: number; maxLength: number; remainingCharacters: number }) => JSX.Element
Custom renderer for displaying character count.
import { CharacterLimitPlugin } from '@lexical/react/LexicalCharacterLimitPlugin';

<CharacterLimitPlugin 
  maxLength={500}
  renderer={({ remainingCharacters }) => (
    <div className="character-count">
      {remainingCharacters} characters remaining
    </div>
  )}
/>

Utility Plugins

EditorRefPlugin

Provides access to the editor instance outside the composer tree.
import { EditorRefPlugin } from '@lexical/react/LexicalEditorRefPlugin';
import { useRef } from 'react';
import { LexicalEditor } from 'lexical';

function App() {
  const editorRef = useRef<LexicalEditor>(null);

  const handleExternalAction = () => {
    if (editorRef.current) {
      editorRef.current.update(() => {
        // Perform actions
      });
    }
  };

  return (
    <>
      <button onClick={handleExternalAction}>External Button</button>
      <LexicalComposer initialConfig={config}>
        <EditorRefPlugin editorRef={editorRef} />
        {/* other plugins */}
      </LexicalComposer>
    </>
  );
}

ClearEditorPlugin

Provides a command to clear all editor content.
import { ClearEditorPlugin } from '@lexical/react/LexicalClearEditorPlugin';
import { CLEAR_EDITOR_COMMAND } from 'lexical';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';

function ClearButton() {
  const [editor] = useLexicalComposerContext();
  
  return (
    <button onClick={() => editor.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined)}>
      Clear
    </button>
  );
}

// In your editor:
<ClearEditorPlugin />

TreeViewPlugin

Displays a debug view of the editor’s node tree.
import { TreeViewPlugin } from '@lexical/react/LexicalTreeView';

<TreeViewPlugin />

Creating Custom Plugins

Basic Plugin Pattern

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useEffect } from 'react';
import { COMMAND_PRIORITY_LOW } from 'lexical';

function CustomPlugin() {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    // Register update listener
    const removeUpdateListener = editor.registerUpdateListener(
      ({ editorState }) => {
        editorState.read(() => {
          // Read editor state
        });
      }
    );

    // Register command
    const removeCommand = editor.registerCommand(
      CUSTOM_COMMAND,
      (payload) => {
        // Handle command
        return false; // or true to stop propagation
      },
      COMMAND_PRIORITY_LOW
    );

    // Cleanup
    return () => {
      removeUpdateListener();
      removeCommand();
    };
  }, [editor]);

  return null;
}

Plugin with UI

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useState, useEffect } from 'react';
import { $getRoot } from 'lexical';

function WordCountPlugin() {
  const [editor] = useLexicalComposerContext();
  const [wordCount, setWordCount] = useState(0);

  useEffect(() => {
    return editor.registerUpdateListener(({ editorState }) => {
      editorState.read(() => {
        const root = $getRoot();
        const text = root.getTextContent();
        const words = text.split(/\s+/).filter(Boolean);
        setWordCount(words.length);
      });
    });
  }, [editor]);

  return (
    <div className="word-count">
      Word count: {wordCount}
    </div>
  );
}

Plugin with Commands

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useEffect } from 'react';
import { createCommand, COMMAND_PRIORITY_NORMAL } from 'lexical';

export const INSERT_EMOJI_COMMAND = createCommand<string>('INSERT_EMOJI');

function EmojiPlugin() {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    return editor.registerCommand(
      INSERT_EMOJI_COMMAND,
      (emoji) => {
        editor.update(() => {
          const selection = $getSelection();
          if ($isRangeSelection(selection)) {
            selection.insertText(emoji);
          }
        });
        return true;
      },
      COMMAND_PRIORITY_NORMAL
    );
  }, [editor]);

  return null;
}

// Usage in another component:
function EmojiButton({ emoji }: { emoji: string }) {
  const [editor] = useLexicalComposerContext();
  return (
    <button onClick={() => editor.dispatchCommand(INSERT_EMOJI_COMMAND, emoji)}>
      {emoji}
    </button>
  );
}

Plugin Best Practices

Single Responsibility

Each plugin should focus on one feature. Split complex functionality into multiple plugins.

Cleanup Functions

Always return cleanup functions from useEffect to unregister listeners and prevent memory leaks.

Command Priorities

Use appropriate command priorities. Higher priority handlers run first and can stop propagation.

State Management

Use React state for UI concerns. Store editor data in EditorState, not React state.
Never mutate the editor state outside of editor.update() or editor.read() callbacks. This will cause inconsistencies and errors.
Use mergeRegister() from @lexical/utils to combine multiple cleanup functions into one.
import { mergeRegister } from '@lexical/utils';

useEffect(() => {
  return mergeRegister(
    editor.registerUpdateListener(...),
    editor.registerCommand(...),
    editor.registerNodeTransform(...)
  );
}, [editor]);

React Hooks

Explore hooks for accessing editor state and functionality

Commands

Learn about the command system in Lexical

Node Transforms

Understand how to use node transforms

Listeners

Deep dive into editor listeners

Build docs developers (and LLMs) love