Skip to main content
The @lexical/rich-text package provides everything needed to build a full-featured rich text editor with headings, quotes, lists, and text formatting.

Setup

1
Install the Package
2
npm install @lexical/rich-text
3
Register Rich Text Nodes
4
import { createEditor } from 'lexical';
import { HeadingNode, QuoteNode } from '@lexical/rich-text';

const editor = createEditor({
  namespace: 'MyEditor',
  nodes: [HeadingNode, QuoteNode],
  onError: (error) => console.error(error),
});
5
Register Rich Text Commands
6
import { registerRichText } from '@lexical/rich-text';

const unregister = registerRichText(editor);

// Cleanup when done
unregister();

React Integration

Use RichTextPlugin for React applications:
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import { HeadingNode, QuoteNode } from '@lexical/rich-text';

function Editor() {
  const initialConfig = {
    namespace: 'RichTextEditor',
    nodes: [HeadingNode, QuoteNode],
    onError: (error: Error) => {
      console.error(error);
    },
  };

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <RichTextPlugin
        contentEditable={<ContentEditable className="editor-input" />}
        placeholder={<div className="editor-placeholder">Enter text...</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
    </LexicalComposer>
  );
}
See packages/lexical-react/src/LexicalRichTextPlugin.tsx for the plugin implementation.

Headings

Creating Headings

import { $createHeadingNode, HeadingTagType } from '@lexical/rich-text';
import { $getSelection, $isRangeSelection } from 'lexical';

editor.update(() => {
  const selection = $getSelection();
  if ($isRangeSelection(selection)) {
    const headingNode = $createHeadingNode('h1');
    selection.insertNodes([headingNode]);
  }
});

Heading Levels

type HeadingTagType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';

const h1 = $createHeadingNode('h1'); // <h1>
const h2 = $createHeadingNode('h2'); // <h2>
const h3 = $createHeadingNode('h3'); // <h3>

Convert to Heading

import { $setBlocksType } from '@lexical/selection';
import { $createHeadingNode } from '@lexical/rich-text';
import { $getSelection, $isRangeSelection } from 'lexical';

editor.update(() => {
  const selection = $getSelection();
  if ($isRangeSelection(selection)) {
    $setBlocksType(selection, () => $createHeadingNode('h2'));
  }
});

HeadingNode Properties

import { HeadingNode } from '@lexical/rich-text';

const heading = $createHeadingNode('h2');
const tag = heading.getTag(); // 'h2'
See packages/lexical-rich-text/src/index.ts:218-330 for HeadingNode implementation.

Quotes

Creating Quotes

import { $createQuoteNode } from '@lexical/rich-text';
import { $getSelection, $isRangeSelection } from 'lexical';

editor.update(() => {
  const selection = $getSelection();
  if ($isRangeSelection(selection)) {
    const quoteNode = $createQuoteNode();
    selection.insertNodes([quoteNode]);
  }
});

Convert to Quote

import { $setBlocksType } from '@lexical/selection';
import { $createQuoteNode } from '@lexical/rich-text';

editor.update(() => {
  const selection = $getSelection();
  if ($isRangeSelection(selection)) {
    $setBlocksType(selection, () => $createQuoteNode());
  }
});

QuoteNode Behavior

  • Pressing Enter at the end creates a new paragraph
  • Pressing Backspace at the start collapses to paragraph
  • Supports nested content
See packages/lexical-rich-text/src/index.ts:127-216 for QuoteNode implementation.

Text Formatting

Format Commands

import { FORMAT_TEXT_COMMAND } from 'lexical';

// Bold
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');

// Italic
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');

// Underline
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');

// Strikethrough
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');

// Code
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');

// Subscript
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript');

// Superscript
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript');

Programmatic Formatting

import { $getSelection, $isRangeSelection } from 'lexical';

editor.update(() => {
  const selection = $getSelection();
  if ($isRangeSelection(selection)) {
    selection.formatText('bold');
  }
});

Check Format State

import { $getSelection, $isRangeSelection } from 'lexical';

editor.getEditorState().read(() => {
  const selection = $getSelection();
  if ($isRangeSelection(selection)) {
    const isBold = selection.hasFormat('bold');
    const isItalic = selection.hasFormat('italic');
    console.log({ isBold, isItalic });
  }
});

Toolbar Example

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $getSelection, $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical';
import { useCallback, useEffect, useState } from 'react';
import { mergeRegister } from '@lexical/utils';

function ToolbarPlugin() {
  const [editor] = useLexicalComposerContext();
  const [isBold, setIsBold] = useState(false);
  const [isItalic, setIsItalic] = useState(false);

  useEffect(() => {
    return mergeRegister(
      editor.registerUpdateListener(({ editorState }) => {
        editorState.read(() => {
          const selection = $getSelection();
          if ($isRangeSelection(selection)) {
            setIsBold(selection.hasFormat('bold'));
            setIsItalic(selection.hasFormat('italic'));
          }
        });
      }),
    );
  }, [editor]);

  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}
        className={isBold ? 'active' : ''}
      >
        Bold
      </button>
      <button
        onClick={formatItalic}
        className={isItalic ? 'active' : ''}
      >
        Italic
      </button>
    </div>
  );
}

Element Formatting

Alignment

import { FORMAT_ELEMENT_COMMAND, ElementFormatType } from 'lexical';

// Left align
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left');

// Center align
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center');

// Right align
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right');

// Justify
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify');

Indentation

import { INDENT_CONTENT_COMMAND, OUTDENT_CONTENT_COMMAND } from 'lexical';

// Indent
editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined);

// Outdent
editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined);

Keyboard Shortcuts

Rich text plugin registers these shortcuts:
  • Bold: Ctrl/Cmd + B
  • Italic: Ctrl/Cmd + I
  • Underline: Ctrl/Cmd + U
  • Undo: Ctrl/Cmd + Z
  • Redo: Ctrl/Cmd + Shift + Z
  • Enter: Insert paragraph
  • Shift + Enter: Insert line break
  • Tab: Indent
  • Shift + Tab: Outdent
  • Backspace: Delete character/merge blocks
See packages/lexical-rich-text/src/index.ts:346-1080 for keyboard command implementations.

Line Breaks vs Paragraphs

Insert Line Break

import { INSERT_LINE_BREAK_COMMAND } from 'lexical';

// Shift + Enter
editor.dispatchCommand(INSERT_LINE_BREAK_COMMAND, false);

Insert Paragraph

import { INSERT_PARAGRAPH_COMMAND } from 'lexical';

// Enter
editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined);

Styling with Themes

Configure theme classes for rich text elements:
const theme = {
  heading: {
    h1: 'editor-heading-h1',
    h2: 'editor-heading-h2',
    h3: 'editor-heading-h3',
    h4: 'editor-heading-h4',
    h5: 'editor-heading-h5',
    h6: 'editor-heading-h6',
  },
  quote: 'editor-quote',
  text: {
    bold: 'editor-text-bold',
    italic: 'editor-text-italic',
    underline: 'editor-text-underline',
    strikethrough: 'editor-text-strikethrough',
    code: 'editor-text-code',
  },
};

const editor = createEditor({
  namespace: 'MyEditor',
  theme,
  // ...
});
Example CSS:
.editor-heading-h1 {
  font-size: 2em;
  font-weight: bold;
  margin: 0.67em 0;
}

.editor-heading-h2 {
  font-size: 1.5em;
  font-weight: bold;
  margin: 0.75em 0;
}

.editor-quote {
  border-left: 4px solid #ccc;
  padding-left: 1em;
  margin: 1em 0;
  color: #666;
}

.editor-text-bold {
  font-weight: bold;
}

.editor-text-italic {
  font-style: italic;
}

.editor-text-code {
  background-color: #f4f4f4;
  padding: 2px 4px;
  border-radius: 3px;
  font-family: monospace;
}

Custom Block Types

Extend with additional block types:
import { ElementNode } from 'lexical';
import { addClassNamesToElement } from '@lexical/utils';

class CalloutNode extends ElementNode {
  static getType(): string {
    return 'callout';
  }

  static clone(node: CalloutNode): CalloutNode {
    return new CalloutNode(node.__key);
  }

  createDOM(config: EditorConfig): HTMLElement {
    const div = document.createElement('div');
    addClassNamesToElement(div, config.theme.callout);
    return div;
  }

  updateDOM(): boolean {
    return false;
  }

  static importJSON(serializedNode: SerializedElementNode): CalloutNode {
    return $createCalloutNode().updateFromJSON(serializedNode);
  }

  exportJSON(): SerializedElementNode {
    return {
      ...super.exportJSON(),
    };
  }
}

function $createCalloutNode(): CalloutNode {
  return new CalloutNode();
}

Best Practices

  • Use Commands: Dispatch commands instead of direct node manipulation
  • Update Listener: Track formatting state for toolbar UI
  • Theme Classes: Use theme system for styling consistency
  • Keyboard Shortcuts: Leverage built-in shortcuts
  • Block Types: Use $setBlocksType() to convert selections
  • Text Formats: Use selection.formatText() for inline formatting

See Also

Build docs developers (and LLMs) love