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.
| Prop | Type | Default | Description |
|---|---|---|---|
value | SerializedEditorState | - | Controlled Lexical JSON document |
defaultValue | SerializedEditorState | - | Initial uncontrolled JSON document |
onValueChange | (value, details) => void | - | Called with JSON plus HTML, text, empty state, character count, and word count |
placeholder | string | "Start writing..." | Placeholder text for empty content |
maxLength | number | - | CharacterCount limit |
isDisabled | boolean | false | Disables editing and controls |
isReadOnly | boolean | false | Keeps content focusable but not editable |
extensions | Klass<LexicalNode>[] | - | Additional Lexical nodes appended after the defaults |
editorOptions | Partial<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.
| Prop | Type | Description |
|---|---|---|
command | "bold" | "italic" | "underline" | "strike" | "code" | "blockquote" | "bulletList" | "orderedList" | "checkList" | "codeBlock" | "heading-1" | "heading-2" | "heading-3" | Formatting command |
tooltip | ReactNode | Optional tooltip content |
RichTextEditor.ActionButton
Runs editor actions.
| Prop | Type | Description |
|---|---|---|
action | "undo" | "redo" | "clearFormatting" | "clearContent" | Editor action |
tooltip | ReactNode | Optional tooltip content |
RichTextEditor.CommandButton
Runs any custom Lexical command.
| Prop | Type | Description |
|---|---|---|
onCommand | (editor) => void | boolean | Command to run with the current editor |
isActive | boolean | ((editor) => boolean) | Active state used for styling and aria-pressed |
isDisabled | boolean | ((editor) => boolean) | Command-specific disabled state |
tooltip | ReactNode | Optional 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.
| Prop | Type | Default | Description |
|---|---|---|---|
toolbarProps | ToolbarProps | - | 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.
| Prop | Type | Default | Description |
|---|---|---|---|
toolbarProps | ToolbarProps | - | 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.
| Prop | Type | Default | Description |
|---|---|---|---|
showAddButton | boolean | true | Show the "+" button that opens the slash menu |
addTriggerChar | string | "/" | 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.
| Prop | Type | Default | Description |
|---|---|---|---|
char | string | "/" | 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 |
allowSpaces | boolean | false | Allows spaces in the query |
maxHeight | number | 384 | Max menu height in pixels |
RichTextEditor.CharacterCount
Displays the editor's character and word counts.
| Prop | Type | Default | Description |
|---|---|---|---|
showWords | boolean | false | Appends word count to the default label |
children | ReactNode | (stats) => ReactNode | - | Custom count rendering |
Hooks
| Hook | Description |
|---|---|
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 |