Creating Custom Extensions

Extend LexKit with powerful custom functionality

Learn how to build type-safe extensions that seamlessly integrate with LexKit's editor system. From simple commands to complex UI components, unlock the full potential of your editor.

Type-SafePowerful APIExtensible

What Are Extensions?

Extensions are modular pieces of functionality that enhance your LexKit editor. They provide commands, state queries, UI components, and custom behavior.

Commands

Define actions that users can trigger. Commands are strongly typed and accessible through the editor's command palette and toolbar.

State Queries

Query the current state of your editor. Use these to enable/disable buttons, show contextual UI, or make decisions based on content.

UI Components

Add toolbar buttons, floating panels, context menus, and other UI elements that integrate seamlessly with your editor.

Extension Anatomy

Let's break down what makes up a LexKit extension.

Method 1: createExtension Function

The functional approach - perfect for simple, focused extensions

The createExtension Function

Every extension starts with the createExtension function. This factory function creates a strongly typed extension instance with proper TypeScript support.

Basic Extension Structure

import { createExtension } from '@lexkit/editor'

const MyExtension = createExtension({
  name: 'my-extension',
  commands: (editor) => ({
    // Define your commands here
    myCommand: () => console.log('Hello!')
  })
})
Commands Object

Define the actions your extension provides. Each command is a function that receives the Lexical editor instance and performs some operation.

commands: (editor) => ({
  insertText: (text: string) => {
    editor.update(() => {
      // Insert text at cursor
      const selection = $getSelection()
      if ($isRangeSelection(selection)) {
        selection.insertText(text)
      }
    })
  },

  clearEditor: () => {
    editor.update(() => {
      $getRoot().clear()
    })
  }
})
State Queries

Define functions that return information about the editor's current state. These are async functions that can read from the editor safely.

stateQueries: (editor) => ({
  hasSelection: async () => {
    return new Promise(resolve => {
      editor.read(() => {
        const selection = $getSelection()
        resolve($isRangeSelection(selection))
      })
    })
  },

  isEmpty: async () => {
    return new Promise(resolve => {
      editor.read(() => {
        resolve(!$getRoot().getTextContent().trim())
      })
    })
  }
})

Try createExtension

Interactive demo of the functional approach. Click the buttons to test extension features!

createExtension Demo

Functional approach - simple and focused

Start typing...

Method 2: BaseExtension Class

The object-oriented approach - ideal for complex extensions with inheritance

Extending BaseExtension

The BaseExtension class provides a more traditional class-based approach to creating extensions. This is perfect for complex extensions that need inheritance or shared state.

Key Benefits:

  • • Traditional class-based architecture
  • • Instance methods and properties
  • • Inheritance and method overriding
  • • Better for complex state management
Commands Implementation

Implement the getCommands method to return your extension's command functions. Each command receives the Lexical editor instance and performs operations.

getCommands(editor: LexicalEditor): TestCommands {
  return {
    insertTimestamp: () => {
      editor.focus();
      const timestamp = new Date().toLocaleString();
      editor.update(() => {
        const root = $getRoot();
        const paragraph = $createParagraphNode();
        paragraph.append($createTextNode(timestamp));
        root.append(paragraph);
      });
    },

    clearContent: () => {
      editor.update(() => {
        $getRoot().clear();
      });
    },

    getWordCount: () => {
      alert('Hello World from BaseExtension!');
    }
  };
}
State Queries Implementation

Implement the getStateQueries method to return async functions that query the editor's current state safely using editor.read().

getStateQueries(editor: LexicalEditor): TestStateQueries {
  return {
    hasSelection: async () => {
      return new Promise(resolve => {
        editor.read(() => {
          const selection = $getSelection();
          resolve($isRangeSelection(selection) && !selection.isCollapsed());
        });
      });
    },

    isEmpty: async () => {
      return new Promise(resolve => {
        editor.read(() => {
          const root = $getRoot();
          resolve(!root.getTextContent().trim());
        });
      });
    }
  };
}
When to Use BaseExtension

Choose BaseExtension when you need:

Perfect For:
  • • Complex state management
  • • Multiple related commands
  • • Inheritance hierarchies
  • • Instance-specific configuration
Use createExtension For:
  • • Simple, functional extensions
  • • Quick prototyping
  • • Single-purpose extensions
  • • Less complex state needs

Try BaseExtension

Interactive demo of the class-based approach. Same functionality, different architecture!

BaseExtension Demo

Class-based approach - perfect for complex extensions

Start typing...

Using Your Extension

Universal Integration: No matter which approach you choose to create your extensions, the integration process is exactly the same. Both createExtension and BaseExtension extensions work identically in your editor system.

Same IntegrationSame APISame Usage
1Define Extensions Array

Create a const assertion array containing your extensions. The process is identical regardless of how you created your extensions - both approaches work the same way.

const extensions = [YourExtension] as const;
2Create Editor System

Use the createEditorSystem function to generate a typed Provider and useEditor hook based on your extensions. The process is identical for both approaches.

const { Provider, useEditor } = createEditorSystem<typeof extensions>();
3Use in Components

Wrap your editor with the Provider and use the useEditor hook to access commands and state queries from your extensions. Works the same way for both approaches.

Using Extension Commands

function MyEditor() {
  const { commands, stateQueries } = useEditor()

  const handleInsertText = () => {
    commands.insertText('Hello World!')
  }

  const handleClear = () => {
    commands.clearEditor()
  }

  const checkSelection = async () => {
    const hasSelection = await stateQueries.hasSelection()
    console.log('Has selection:', hasSelection)
  }

  return (
    <div>
      <div className="mb-4 space-x-2">
        <button onClick={handleInsertText}>
          Insert Text
        </button>
        <button onClick={handleClear}>
          Clear Editor
        </button>
        <button onClick={checkSelection}>
          Check Selection
        </button>
      </div>

      <EditorContent />
    </div>
  )
}

Best Practices

Tips for building maintainable and performant extensions.

Type Safety First

Leverage TypeScript's power. Define clear interfaces for your commands and state queries. This prevents runtime errors and improves developer experience.

Proper Cleanup

Always return a cleanup function from your initialize method. This ensures event listeners and other resources are properly disposed of.

Performance Matters

Use editor.read() for reading operations and editor.update()for mutations. Avoid unnecessary re-renders and optimize state queries.

Documentation

Document your extension's API clearly. Include JSDoc comments for commands and state queries to help other developers understand your extension.