Skip to main content
Plugins are React components that hook into the Lexical editor lifecycle to add functionality like toolbars, autocomplete, custom commands, and more.

Basic Plugin Structure

A Lexical plugin is a React component that:
  • Uses useLexicalComposerContext() to access the editor
  • Registers listeners, commands, or transforms in useEffect
  • Returns null (or optional UI)
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useEffect } from 'react';

function MyPlugin(): null {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    // Register listeners, commands, transforms
    return editor.registerUpdateListener(({ editorState }) => {
      console.log('Editor updated!');
    });
  }, [editor]);

  return null;
}

AutoFocus Plugin Example

A simple plugin that focuses the editor on mount:
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useEffect } from 'react';

type Props = {
  defaultSelection?: 'rootStart' | 'rootEnd';
};

export function AutoFocusPlugin({ defaultSelection }: Props): null {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    editor.focus(
      () => {
        const activeElement = document.activeElement;
        const rootElement = editor.getRootElement();
        if (
          rootElement !== null &&
          (activeElement === null || !rootElement.contains(activeElement))
        ) {
          rootElement.focus({ preventScroll: true });
        }
      },
      { defaultSelection },
    );
  }, [defaultSelection, editor]);

  return null;
}
See packages/lexical-react/src/LexicalAutoFocusPlugin.ts for the complete implementation.

Command Plugin Example

Register custom commands to handle user actions:
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useEffect } from 'react';
import {
  COMMAND_PRIORITY_EDITOR,
  createCommand,
  LexicalCommand,
} from 'lexical';

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

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

  useEffect(() => {
    return editor.registerCommand(
      INSERT_EMOJI_COMMAND,
      (emoji: string) => {
        editor.update(() => {
          const selection = $getSelection();
          if ($isRangeSelection(selection)) {
            const emojiNode = $createEmojiNode('emoji', emoji);
            selection.insertNodes([emojiNode]);
          }
        });
        return true;
      },
      COMMAND_PRIORITY_EDITOR,
    );
  }, [editor]);

  return null;
}

Transform Plugin Example

Node transforms are called automatically when nodes of a specific type change:
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useEffect } from 'react';
import { TextNode } from 'lexical';

function MarkdownLinkPlugin(): null {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    return editor.registerNodeTransform(TextNode, (textNode) => {
      const text = textNode.getTextContent();
      // Match [text](url) pattern
      const match = text.match(/\[([^\]]+)\]\(([^)]+)\)/);
      
      if (match) {
        const [fullMatch, linkText, url] = match;
        const linkNode = $createLinkNode(url);
        const textNodeForLink = $createTextNode(linkText);
        linkNode.append(textNodeForLink);
        
        // Replace the matched text with link node
        textNode.replace(linkNode);
      }
    });
  }, [editor]);

  return null;
}

Update Listener Plugin

React to editor state changes:
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useEffect, useState } from 'react';
import { $getRoot } from 'lexical';

function WordCountPlugin(): JSX.Element {
  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.trim().split(/\s+/).filter(Boolean);
        setWordCount(words.length);
      });
    });
  }, [editor]);

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

Mutation Listener Plugin

Listen to DOM mutations for specific nodes:
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useEffect } from 'react';
import { ImageNode } from './nodes/ImageNode';

function ImageLoadPlugin(): null {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    return editor.registerMutationListener(ImageNode, (mutatedNodes) => {
      for (let [nodeKey, mutation] of mutatedNodes) {
        if (mutation === 'created') {
          console.log('Image node created:', nodeKey);
        }
      }
    });
  }, [editor]);

  return null;
}

Plugin with UI

Plugins can also render UI elements:
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useCallback } from 'react';
import { FORMAT_TEXT_COMMAND } from 'lexical';

function ToolbarPlugin(): JSX.Element {
  const [editor] = useLexicalComposerContext();

  const formatBold = useCallback(() => {
    editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
  }, [editor]);

  const formatItalic = useCallback(() => {
    editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
  }, [editor]);

  return (
    <div className="toolbar">
      <button onClick={formatBold}>Bold</button>
      <button onClick={formatItalic}>Italic</button>
    </div>
  );
}

Using Plugins

1
Import the Plugin
2
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
import { MyCustomPlugin } from './plugins/MyCustomPlugin';
3
Add to Editor Component
4
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';

function Editor() {
  return (
    <LexicalComposer initialConfig={config}>
      <RichTextPlugin
        contentEditable={<ContentEditable />}
        placeholder={<div>Enter text...</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
      <AutoFocusPlugin />
      <MyCustomPlugin />
    </LexicalComposer>
  );
}

Plugin Cleanup

Always return cleanup functions from useEffect to avoid memory leaks:
function MyPlugin(): null {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    // Register multiple listeners
    const removeUpdateListener = editor.registerUpdateListener(({ editorState }) => {
      // Handle updates
    });

    const removeCommand = editor.registerCommand(
      MY_COMMAND,
      (payload) => {
        // Handle command
        return false;
      },
      COMMAND_PRIORITY_EDITOR,
    );

    // Return cleanup function
    return () => {
      removeUpdateListener();
      removeCommand();
    };
  }, [editor]);

  return null;
}
Or use mergeRegister from @lexical/utils:
import { mergeRegister } from '@lexical/utils';

function MyPlugin(): null {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    return mergeRegister(
      editor.registerUpdateListener(({ editorState }) => {
        // Handle updates
      }),
      editor.registerCommand(
        MY_COMMAND,
        (payload) => {
          // Handle command
          return false;
        },
        COMMAND_PRIORITY_EDITOR,
      ),
    );
  }, [editor]);

  return null;
}

Common Plugin Patterns

Toolbar Plugin

  • Renders formatting buttons
  • Dispatches FORMAT_TEXT_COMMAND
  • Listens to selection changes to update button states

Autocomplete Plugin

  • Listens to text input
  • Shows suggestions based on context
  • Inserts nodes when suggestion is selected

Validation Plugin

  • Registers node transforms
  • Enforces content rules
  • Prevents invalid node structures

Analytics Plugin

  • Registers update listeners
  • Tracks user interactions
  • Sends events to analytics service

Best Practices

  • Return Cleanup: Always return cleanup functions from useEffect
  • Use mergeRegister: Simplify cleanup with mergeRegister utility
  • Dependencies: Include editor in useEffect dependencies
  • Command Priority: Use appropriate priority levels for commands
  • Performance: Debounce expensive operations in update listeners
  • Error Handling: Wrap risky operations in try-catch blocks

See Also

Build docs developers (and LLMs) love