27.7k

Rich Text Editor

A composable Lexical editor with toolbar controls, selection and empty-line menus, slash commands, link popover, JSON value updates, and character count.

Import

import { RichTextEditor } from '@darkcode-ui/react';

DarkCode UI builds the Rich Text Editor on Lexical. The value model is JSON-first: value/defaultValue are Lexical SerializedEditorState documents, and onValueChange also reports HTML, plain text, empty state, and character/word counts.

Anatomy

Compose the editor from its parts with dot notation. The root owns the Lexical editor instance and shares it with every part through context.

<RichTextEditor defaultValue={documentJson} onValueChange={setDocumentJson}>
  <RichTextEditor.Shell>
    <RichTextEditor.Toolbar>
      <RichTextEditor.ToolbarGroup>
        <RichTextEditor.ToggleButton command="bold" />
        <RichTextEditor.ToggleButton command="italic" />
        <RichTextEditor.LinkPopover>
          <RichTextEditor.LinkPopover.Trigger />
          <RichTextEditor.LinkPopover.Content>
            <RichTextEditor.LinkPopover.Input />
            <RichTextEditor.LinkPopover.Actions>
              <RichTextEditor.LinkPopover.UnsetButton />
              <RichTextEditor.LinkPopover.ApplyButton />
            </RichTextEditor.LinkPopover.Actions>
          </RichTextEditor.LinkPopover.Content>
        </RichTextEditor.LinkPopover>
      </RichTextEditor.ToolbarGroup>
    </RichTextEditor.Toolbar>

    <RichTextEditor.Content />

    <RichTextEditor.BubbleMenu>
      <RichTextEditor.ToggleButton command="bold" />
    </RichTextEditor.BubbleMenu>

    <RichTextEditor.Footer>
      <RichTextEditor.CharacterCount />
    </RichTextEditor.Footer>
  </RichTextEditor.Shell>
</RichTextEditor>

Usage

"use client";

import {RichTextEditor} from "@darkcode-ui/react";

import {EditorBubbleMenu, EditorToolbar} from "./toolbar";

Controlled

Pass value and onValueChange to control the document. The handler receives the JSON document plus a details object ({ json, html, text, isEmpty, characterCount, wordCount }).

"use client";

import type {RichTextEditorChangeDetails, RichTextEditorValue} from "@darkcode-ui/react";

import {RichTextEditor} from "@darkcode-ui/react";

Character Count

Add maxLength and render RichTextEditor.CharacterCount in the footer. It exposes a data-over-limit attribute when the limit is exceeded.

"use client";

import {RichTextEditor} from "@darkcode-ui/react";

import {EditorBubbleMenu, EditorToolbar} from "./toolbar";

Placeholder

"use client";

import {RichTextEditor} from "@darkcode-ui/react";

import {EditorToolbar} from "./toolbar";

Disabled And Read Only

isDisabled dims the editor and blocks interaction; isReadOnly keeps the content focusable but not editable.

"use client";

import {RichTextEditor} from "@darkcode-ui/react";
import {$createQuoteNode} from "@lexical/rich-text";
import {$createParagraphNode, $createTextNode, $getRoot} from "lexical";

Custom Composition

Rearrange the parts freely — drop the toolbar, keep only the bubble menu, add a footer, and so on.

"use client";

import {RichTextEditor} from "@darkcode-ui/react";

export function CustomComposition() {

Extensible Commands

Use CommandButton, FloatingMenu, SuggestionMenu, and the editor hooks to wire custom Lexical commands without waiting for a new built-in prop.

"use client";

import {
  IconCalendar,
  IconCode,
import { RichTextEditor, filterRichTextEditorSuggestionItems } from '@darkcode-ui/react';
import { $createHeadingNode } from '@lexical/rich-text';
import { $setBlocksType } from '@lexical/selection';
import { $getSelection, $isRangeSelection } from 'lexical';

const slashItems = ({ query }) =>
  filterRichTextEditorSuggestionItems(
    [
      {
        title: 'Heading 1',
        keywords: ['title', 'h1'],
        command: ({ editor }) =>
          editor.update(() => {
            const selection = $getSelection();

            if ($isRangeSelection(selection)) {
              $setBlocksType(selection, () => $createHeadingNode('h1'));
            }
          }),
      },
    ],
    query,
  );

<RichTextEditor extensions={customNodes}>
  <RichTextEditor.Shell>
    <RichTextEditor.Toolbar>
      <RichTextEditor.CommandButton
        aria-label="Insert date"
        onCommand={(editor) =>
          editor.update(() => {
            $getSelection()?.insertText('27 May 2026');
          })
        }
      />
    </RichTextEditor.Toolbar>
    <RichTextEditor.Content />
    <RichTextEditor.SuggestionMenu char="/" items={slashItems} />
  </RichTextEditor.Shell>
</RichTextEditor>;

Because DarkCode UI uses Lexical (not Tiptap), suggestion-item commands receive { editor } and the trigger query text is removed for you before the command runs.

Notion-style block editing

Drop in RichTextEditor.DraggableBlocks for a Notion-like gutter — hover a line to reveal a drag handle (reorder whole blocks) and a "+" button (opens the slash menu). The editor also ships checklist items (the checkList command), a callout block, and a horizontal-rule divider, so a slash menu can offer the full block palette.

"use client";

import {
  $createCalloutNode,
  INSERT_CHECK_LIST_COMMAND,

CSS Classes

Base Classes

  • .rich-text-editor – Root wrapper
  • .rich-text-editor__shell – Editor surface

Element Classes

  • .rich-text-editor__toolbar – Toolbar container
  • .rich-text-editor__toolbar-group – Toolbar group
  • .rich-text-editor__toolbar-button – Toolbar button slot
  • .rich-text-editor__toolbar-separator – Toolbar separator
  • .rich-text-editor__content – Lexical content host
  • .rich-text-editor__prosemirror – Editable element
  • .rich-text-editor__bubble-menu – Floating selection toolbar
  • .rich-text-editor__bubble-menu-toolbar – Toolbar inside the floating selection menu
  • .rich-text-editor__floating-menu – Floating empty-line toolbar
  • .rich-text-editor__floating-menu-toolbar – Toolbar inside the floating empty-line menu
  • .rich-text-editor__suggestion-menu – Triggered suggestion menu
  • .rich-text-editor__suggestion-menu-item – Suggestion menu item
  • .rich-text-editor__footer – Footer row
  • .rich-text-editor__character-count – Character and word count
  • .rich-text-editor__link-popover – Link popover surface
  • .rich-text-editor__link-input – Link URL input
  • .rich-text-editor__block-menu – Block gutter (drag handle + add button)
  • .rich-text-editor__drag-handle – Drag-to-reorder handle
  • .rich-text-editor__add-block – "+" add-block button
  • .rich-text-editor__drag-target-line – Drop indicator line
  • .rich-text-editor__callout – Callout block
  • .rich-text-editor__divider – Horizontal-rule divider
  • .rich-text-editor__listitem-checked / .rich-text-editor__listitem-unchecked – Checklist items

States

  • Disabled: [data-disabled="true"] on .rich-text-editor
  • Read only: [data-readonly="true"] on .rich-text-editor
  • Over limit: [data-over-limit="true"] on .rich-text-editor__character-count
  • Active command: [data-active="true"] on toolbar buttons

API Reference

RichTextEditor

The root provider. It owns the Lexical editor instance and emits JSON-first value updates.

PropTypeDefaultDescription
valueSerializedEditorState-Controlled Lexical JSON document
defaultValueSerializedEditorState-Initial uncontrolled JSON document
onValueChange(value, details) => void-Called with JSON plus HTML, text, empty state, character count, and word count
placeholderstring"Start writing..."Placeholder text for empty content
maxLengthnumber-CharacterCount limit
isDisabledbooleanfalseDisables editing and controls
isReadOnlybooleanfalseKeeps content focusable but not editable
extensionsKlass<LexicalNode>[]-Additional Lexical nodes appended after the defaults
editorOptionsPartial<InitialConfigType>-Advanced Lexical composer options merged after defaults

The details object passed to onValueChange is { json, html, text, isEmpty, characterCount, wordCount }.

RichTextEditor.ToggleButton

Runs a formatting command and subscribes to active/disabled editor state.

PropTypeDescription
command"bold" | "italic" | "underline" | "strike" | "code" | "blockquote" | "bulletList" | "orderedList" | "checkList" | "codeBlock" | "heading-1" | "heading-2" | "heading-3"Formatting command
tooltipReactNodeOptional tooltip content

RichTextEditor.ActionButton

Runs editor actions.

PropTypeDescription
action"undo" | "redo" | "clearFormatting" | "clearContent"Editor action
tooltipReactNodeOptional tooltip content

RichTextEditor.CommandButton

Runs any custom Lexical command.

PropTypeDescription
onCommand(editor) => void | booleanCommand to run with the current editor
isActiveboolean | ((editor) => boolean)Active state used for styling and aria-pressed
isDisabledboolean | ((editor) => boolean)Command-specific disabled state
tooltipReactNodeOptional tooltip content

RichTextEditor.LinkPopover

Compound popover for applying and removing links through Lexical's link commands. Subcomponents: Trigger, Content, Input, Actions, ApplyButton, and UnsetButton.

RichTextEditor.Content

Renders the Lexical ContentEditable and placeholder for the current editor instance.

RichTextEditor.BubbleMenu

A floating selection toolbar. By default it appears for non-empty text selections.

PropTypeDefaultDescription
toolbarPropsToolbarProps-Props forwarded to the internal selection toolbar

RichTextEditor.FloatingMenu

A floating empty-line toolbar. By default it appears on an empty editable block while the editor is focused.

PropTypeDefaultDescription
toolbarPropsToolbarProps-Props forwarded to the internal insertion toolbar

RichTextEditor.DraggableBlocks

A Notion-style block gutter. Mount it inside Shell; it adds a hover drag handle (reorder whole blocks) and an optional "+" add-block button.

PropTypeDefaultDescription
showAddButtonbooleantrueShow the "+" button that opens the slash menu
addTriggerCharstring"/"Character inserted by the add button (empty string to skip)

RichTextEditor.SuggestionMenu

Registers a Lexical typeahead plugin for slash commands, mentions, and other triggered menus. Items with { title, description, icon, keywords, command } render in the default menu.

PropTypeDefaultDescription
charstring"/"Trigger character
items({ query, editor }) => Item[] | Promise<Item[]>-Suggestion item loader
children(props) => ReactNode-Custom renderer for complete menu control
onSelect(props) => void-Selection handler for custom item shapes
allowSpacesbooleanfalseAllows spaces in the query
maxHeightnumber384Max menu height in pixels

RichTextEditor.CharacterCount

Displays the editor's character and word counts.

PropTypeDefaultDescription
showWordsbooleanfalseAppends word count to the default label
childrenReactNode | (stats) => ReactNode-Custom count rendering

Hooks

HookDescription
useRichTextEditor()Returns { editor, isDisabled, isReadOnly, maxLength } from the nearest editor
useRichTextEditorState(selector, equalityFn)Subscribes to selected Lexical editor state without re-rendering for unrelated edits

On this page