Build Your Own Rich Text Editor

This guide shows you how to create a fast, modular rich text editor using Lexical. It is written clearly and includes a full example, plus we created a custom plugin to show how we can create a new plugin and add it to the lexical editor

Overview — What you will learn

By following this guide you will:

  • Create a React project and install Lexical
  • Wire up a basic Lexical editor
  • Add a simple toolbar (bold, italic, underline)
  • Create a custom plugin (Uppercase) — a feature not provided out-of-the-box
  • Understand how to extend Lexical with nodes and plugins

What is Lexical and why use it?

Lexical is a modern editor framework (by Meta) that gives you a small core and lets you add features as plugins. It is designed to be:

  • Fast
  • Modular
  • Themeable (you control the CSS)
  • Extensible (create your own plugins and nodes)

Think of Lexical as a toolbox: the core gives you the editor surface, and plugins are tools you add when you need extra capabilities.

Step 1 — Create the React project and install Lexical

Open a terminal and run:

npx create-react-app lexical-editor
cd lexical-editor

Then install Lexical packages:

npm install lexical @lexical/react

Step 2 — Create a simple Lexical editor (Editor.js)

This file wires up the editor with a minimal configuration. It shows the basic pieces: the LexicalComposer, a rich text plugin, history, and an on-change plugin.

Editor.js

        
        import React from "react";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";

function Editor() {

  const editorConfig = {
    namespace: "MyEditor",
    theme: {},
    onError(error) {
      console.error(error);
    }
  };

  return (
    <LexicalComposer initialConfig={editorConfig}>
      <RichTextPlugin
        contentEditable={<ContentEditable />}
        placeholder={<div>Start typing here...</div>}
      />

      <HistoryPlugin />
      <OnChangePlugin onChange={() => {}} />
    </LexicalComposer>
  );
}

export default Editor;
      

Save this file as src/Editor.js. This gives you a working but minimal editor — think of it as the blank notebook.

Step 3 — Add a toolbar (Bold, Italic, Underline)

A toolbar is a set of buttons that dispatch commands to the editor. The example below is simple and demonstrates how to call the core formatting command.

ToolbarPlugin.js

import React from "react";
import { FORMAT_TEXT_COMMAND } from "lexical";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";

export default function ToolbarPlugin() {
  const [editor] = useLexicalComposerContext();

  const format = (type) => {
    editor.dispatchCommand(FORMAT_TEXT_COMMAND, type);
  };

  return (
    <div className="toolbar">
      <button onClick={() => format("bold")}>Bold</button>
      <button onClick={() => format("italic")}>Italic</button>
      <button onClick={() => format("underline")}>Underline</button>
    </div>
  );
}

Import and include this plugin in your Editor.js (place it above the editor area):

import ToolbarPlugin from "./ToolbarPlugin";

...

<ToolbarPlugin />

Now the editor has basic text-formatting controls.

Step 4 — Register nodes and create a theme (optional but common)

When you want support for more content types (tables, code blocks, headings), you register nodes in the editor config. You also define a theme object that maps editor elements to CSS classnames (your stylesheet will define how those classnames look).

Example editor config (registering nodes)

const editorConfig = {
  theme: ExampleTheme,
  onError(error) {
    throw error;
  },
  nodes: [
    HeadingNode,
    ListNode,
    ListItemNode,
    QuoteNode,
    CodeNode,
    CodeHighlightNode,
    TableNode,
    TableCellNode,
    TableRowNode,
    AutoLinkNode,
    LinkNode
  ]
};

Example theme object (map to your CSS)

const exampleTheme = {
  ltr: "ltr",
  rtl: "rtl",
  placeholder: "editor-placeholder",
  paragraph: "editor-paragraph",
  quote: "editor-quote",
  heading: {
    h1: "editor-heading-h1",
    h2: "editor-heading-h2",
    h3: "editor-heading-h3"
  },
  list: {
    nested: {
      listitem: "editor-nested-listitem"
    },
    ol: "editor-list-ol",
    ul: "editor-list-ul",
    listitem: "editor-listitem"
  },
  link: "editor-link",
  text: {
    bold: "editor-text-bold",
    italic: "editor-text-italic",
    underline: "editor-text-underline",
    code: "editor-text-code"
  },
  code: "editor-code",
  codeHighlight: {
    keyword: "editor-tokenAttr",
    string: "editor-tokenSelector",
    number: "editor-tokenProperty",
    comment: "editor-tokenComment",
    function: "editor-tokenFunction"
  }
};

export default exampleTheme;

Add the nodes array and theme reference to the initialConfig you pass to LexicalComposer when you need those features. If you don't register a node, the editor won't know how to render or handle that node type.

Step 5 — Build a custom plugin (Uppercase)

This part shows how to create a small plugin that converts the selected text into uppercase. It's simple but demonstrates three important ideas:

  1. Read the selection
  2. Update editor state
  3. Insert or replace text

We will build a plugin called UppercasePlugin.

UppercasePlugin.js

import React from "react";
import { $getSelection, $isRangeSelection } from "lexical";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";

export default function UppercasePlugin() {
  const [editor] = useLexicalComposerContext();

  const makeUppercase = () => {
    editor.update(() => {
      const selection = $getSelection();
      if ($isRangeSelection(selection)) {
        // Get the selected text content
        const text = selection.getTextContent();

        // Convert to uppercase
        const upper = text.toUpperCase();

        // Replace the selection with the uppercase text
        selection.insertText(upper);
      }
    });
  };

  return (
    <button onClick={makeUppercase}>
      Uppercase
    </button>
  );
}

How it works (plain language):

  • We ask the editor for the current selection.
  • If the selection is a normal text range, we read the text.
  • We transform the text (to uppercase).
  • We insert the new text back into the editor, replacing the old selection.

Add the plugin to your editor just like the toolbar:

import UppercasePlugin from "./UppercasePlugin";

...

<ToolbarPlugin />
<UppercasePlugin />

Now users can select text and click "Uppercase" to transform it. That exact behavior — a new command implemented by you — is a typical custom plugin pattern in Lexical.

Step 6 — Full editor component (example)

Below is one complete example that brings the pieces together. This is an example for src/App.js (you can import the plugin files from ./ToolbarPlugin and ./UppercasePlugin).

App.js

import React from "react";
import Editor from "./Editor";

function App() {
  return (
    <div>
      <h1>My Lexical Editor</h1>
      <Editor />
    </div>
  );
}

export default App;

And the updated Editor.js that uses the toolbar and plugin:

Editor.js (with toolbar + plugin)

import React from "react";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";

import ToolbarPlugin from "./ToolbarPlugin";
import UppercasePlugin from "./UppercasePlugin";

function Editor() {

  const editorConfig = {
    namespace: "MyEditor",
    theme: {},
    onError(error) {
      console.error(error);
    }
  };

  return (
    <LexicalComposer initialConfig={editorConfig}>
      <div className="editor-shell">

        <ToolbarPlugin />
        <UppercasePlugin />

        <div className="editor-area">
          <RichTextPlugin
            contentEditable={<ContentEditable className="editor-input" />}
            placeholder={<div>Start typing here...</div>}
          />

          <HistoryPlugin />
          <OnChangePlugin onChange={() => { /* handle editor state changes if you need */ }} />
        </div>

      </div>
    </LexicalComposer>
  );
}

export default Editor;

Place ToolbarPlugin.js and UppercasePlugin.js in the same folder (src/), then import them as shown.

Why this approach is useful

Small core, many plugins: ship only what you need — keep the editor fast and small.

Themeable: the editor gives you class names; you write the CSS to match your app.

Extensible: build custom features (like the Uppercase plugin) that suit your product.

Next steps — What to try after this tutorial

  • Add a Link plugin (open a small dialog to edit the URL)
  • Add an Image upload plugin (upload files and insert image nodes)
  • Add Code block highlighting (register a code node and use a syntax highlighter)
  • Persist editor content to your backend (save HTML or JSON)
  • Write tests for your plugins

Troubleshooting & tips

  • If your formatting buttons do nothing: ensure you are using the right command (e.g., FORMAT_TEXT_COMMAND) and that the plugin is inside the LexicalComposer tree.
  • If a node doesn't render: check that the node is registered in the initialConfig.nodes array.
  • Use editor.update() for any modifications to the editor state.
  • For debugging: the community TreeViewPlugin is helpful to see the editor state tree.