Rich Text Editor

A composable Tiptap editor with toolbar controls, selection bubble menu, JSON value updates, and character count support.

Usage

"use client";

import {
  CodeSquareIcon,
  Delete02Icon,

Anatomy

Import the RichTextEditor component and arrange its parts with dot notation.

import { RichTextEditor } from "@thenamespace/uikit";

<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>;

Controlled

{
  "type": "doc",
  "content": [
    {
      "attrs": {
        "level": 2
      },
      "content": [
        {
          "text": "Controlled JSON value",
          "type": "text"
        }
      ],
      "type": "heading"
    },
    {
      "type": "paragraph",
      "content": [
        {
          "text": "This demo keeps the editor document in React state. The buttons above replace the ",
          "type": "text"
        },
        {
          "marks": [
            {
              "type": "code"
            }
          ],
          "text": "value",
          "type": "text"
        },
        {
          "text": " prop, and the JSON preview below updates after every edit.",
          "type": "text"
        }
      ]
    },
    {
      "content": [
        {
          "content": [
            {
              "content": [
                {
                  "text": "Edit this paragraph and watch the JSON mirror change.",
                  "type": "text"
                }
              ],
              "type": "paragraph"
            }
          ],
          "type": "listItem"
        },
        {
          "content": [
            {
              "content": [
                {
                  "text": "The footer reads ",
                  "type": "text"
                },
                {
                  "marks": [
                    {
                      "type": "bold"
                    }
                  ],
                  "text": "character",
                  "type": "text"
                },
                {
                  "text": " and ",
                  "type": "text"
                },
                {
                  "marks": [
                    {
                      "type": "italic"
                    }
                  ],
                  "text": "word",
                  "type": "text"
                },
                {
                  "text": " counts from Tiptap storage.",
                  "type": "text"
                }
              ],
              "type": "paragraph"
            }
          ],
          "type": "listItem"
        }
      ],
      "type": "bulletList"
    },
    {
      "content": [
        {
          "content": [
            {
              "text": "Controlled mode should feel editable, not fragile: external updates only replace content when the incoming JSON differs.",
              "type": "text"
            }
          ],
          "type": "paragraph"
        }
      ],
      "type": "blockquote"
    },
    {
      "type": "paragraph"
    }
  ]
}
"use client";

import { useState } from "react";

import {

Character Count

"use client";

import {
  CodeSquareIcon,
  Delete02Icon,

Placeholder

"use client";

import {
  CodeSquareIcon,
  Delete02Icon,

Disabled And Read Only

Custom Composition

"use client";

import {
  CodeSquareIcon,
  Delete02Icon,

Extensible Commands

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

import {
  RichTextEditor,
  filterRichTextEditorSuggestionItems,
} from "@thenamespace/uikit";

const slashItems = ({ query }) =>
  filterRichTextEditorSuggestionItems(
    [
      {
        title: "Heading 1",
        keywords: ["title", "h1"],
        command: ({ editor, range }) => {
          editor
            .chain()
            .focus()
            .deleteRange(range)
            .setNode("heading", { level: 1 })
            .run();
        },
      },
    ],
    query,
  );

<RichTextEditor extensions={customExtensions}>
  <RichTextEditor.Shell>
    <RichTextEditor.Toolbar>
      <RichTextEditor.CommandButton
        aria-label="Insert date"
        onCommand={(editor) =>
          editor.chain().focus().insertContent("27 May 2026").run()
        }
      />
    </RichTextEditor.Toolbar>
    <RichTextEditor.Content />
    <RichTextEditor.FloatingMenu>
      <RichTextEditor.CommandButton
        aria-label="Add heading"
        onCommand={(editor) =>
          editor.chain().focus().toggleHeading({ level: 2 }).run()
        }
      />
    </RichTextEditor.FloatingMenu>
    <RichTextEditor.SuggestionMenu char="/" items={slashItems} />
  </RichTextEditor.Shell>
</RichTextEditor>;

Disabled

"use client";

import {
  CodeSquareIcon,
  Delete02Icon,

Extensible

// @ts-nocheck -- Complex demo data intentionally uses heterogeneous shapes.
"use client";

import {
  CodeSquareIcon,

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 - Tiptap content host
  • .rich-text-editor__prosemirror - 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

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 Tiptap editor instance and emits JSON-first value updates.

PropTypeDefaultDescription
valueJSONContent-Controlled Tiptap JSON document
defaultValueJSONContent-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
extensionsExtensions-Additional Tiptap extensions appended after defaults
editorOptionsPartial<EditorOptions>-Advanced Tiptap editor options

RichTextEditor.ToggleButton

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

PropTypeDescription
command"bold" | "italic" | "underline" | "strike" | "code" | "blockquote" | "bulletList" | "orderedList" | "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 Tiptap command.

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

RichTextEditor.LinkPopover

Compound popover for applying and removing links through Tiptap link commands.

Subcomponents: Trigger, Content, Input, Actions, ApplyButton, and UnsetButton.

RichTextEditor.Content

Renders the Tiptap EditorContent for the current editor instance.

RichTextEditor.BubbleMenu

Wraps Tiptap's React BubbleMenu and renders its children inside a Namespace UIKit Toolbar. The default visibility shows the menu for non-empty text selections while the editor is focused.

PropTypeDefaultDescription
toolbarPropsToolbarProps-Props forwarded to the internal selection toolbar

RichTextEditor.FloatingMenu

Wraps Tiptap's React FloatingMenu and renders its children inside a Namespace UIKit Toolbar. By default, Tiptap shows this menu on an empty editable text block.

PropTypeDefaultDescription
toolbarPropsToolbarProps-Props forwarded to the internal insertion toolbar

RichTextEditor.SuggestionMenu

Registers a Tiptap suggestion plugin for slash commands, mentions, variables, snippets, and other triggered menus. If no render function is provided, items with {title, description, icon, 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
pluginKeystring | PluginKey-Stable ProseMirror plugin key
allowSpacesbooleanfalseAllows spaces in the query
allowedPrefixesstring[] | null[" "]Prefixes allowed before the trigger
startOfLinebooleanfalseRestricts matches to the start of a line
maxHeightnumber384Max menu height in pixels

Hooks

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

RichTextEditor.CharacterCount

Displays the CharacterCount extension storage values.

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