Skip to Content
Documentation
Getting startedComponentsChartsTheming
Get Pro
Overview
Concepts

Rich Text Editor

Used to write formatted text with a composable toolbar

Source

Usage

The Rich Text Editor is built on Tiptap, a headless editor on top of ProseMirror. Install the component with the CLI, then add Tiptap and the extensions you want to use.

pnpm dlx @saas-ui/cli@rc add rich-text-editor
pnpm add @tiptap/react @tiptap/starter-kit
import { Control, RichTextEditor } from '#components/ui/rich-text-editor'

Create the editor with Tiptap's useEditor hook and pass it to RichTextEditor.Root. Everything below the root reads the editor from context, so the toolbar controls need no props.

import { useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function Editor() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Hello</p>',
    shouldRerenderOnTransaction: true,
    immediatelyRender: false,
  })

  if (!editor) return null

  return (
    <RichTextEditor.Root editor={editor}>
      <RichTextEditor.Toolbar>
        <RichTextEditor.ControlGroup>
          <Control.Bold />
          <Control.Italic />
        </RichTextEditor.ControlGroup>
      </RichTextEditor.Toolbar>

      <RichTextEditor.Content />
    </RichTextEditor.Root>
  )
}
info
Set shouldRerenderOnTransaction so the controls follow the selection, and immediatelyRender: false to avoid a hydration mismatch in server rendered apps.

Anatomy

<RichTextEditor.Root>
  <RichTextEditor.Toolbar>
    <RichTextEditor.ControlGroup />
  </RichTextEditor.Toolbar>

  <RichTextEditor.Content />

  <RichTextEditor.Footer />
</RichTextEditor.Root>

ControlGroup groups related controls, and the toolbar draws a separator between the groups.

Controls

Control is a namespace of ready-made controls. Each one maps to a Tiptap command and reflects the current selection, so Control.Bold renders as active while the cursor sits inside bold text.

ControlRequires
Bold Italic Underline Strikethrough Code@tiptap/starter-kit
H1 H2 H3 H4 TextStyle BulletList OrderedList Blockquote Hr@tiptap/starter-kit
Link Unlink@tiptap/starter-kit
Undo Redo@tiptap/starter-kit
AlignLeft AlignCenter AlignRight AlignJustify@tiptap/extension-text-align
Highlight@tiptap/extension-highlight

A control only works when its extension is registered on the editor. The base set comes from StarterKit, the rest are opt-in.

Custom controls

Use the control factories to add your own. They take care of the tooltip, the icon button and reading the editor from context.

import { LuListChecks } from 'react-icons/lu'

import { createBooleanControl } from '#components/ui/rich-text-editor'

const ToggleTaskList = createBooleanControl({
  label: 'Toggle Task List',
  icon: LuListChecks,
  command: (editor) => editor.chain().focus().toggleTaskList().run(),
  getVariant: (editor) => (editor.isActive('taskList') ? 'subtle' : 'ghost'),
})

createSelectControl builds a dropdown for mutually exclusive states like the block type, and createSwatchControl builds a color picker popover. For a one-off button, render Control.ButtonControl directly and read the editor with useRichTextEditorContext.

Examples

Controlled

Pass onUpdate to keep the document in React state. editor.getHTML() returns the serialized document, editor.getJSON() returns the ProseMirror node.

Placeholder

Add the Placeholder extension from @tiptap/extensions to show a hint while the document is empty.

Task list

The editor styles task lists out of the box. Add @tiptap/extension-task-list and @tiptap/extension-task-item, then build the controls with createBooleanControl.

Images

Add @tiptap/extension-image and insert images with setImage. This example combines a URL field and a file upload in a dialog.

Bubble menu

Wrap a toolbar in Tiptap's BubbleMenu to float the controls above the selection. Use the floating toolbar variant so it renders as a panel.

Props

Root

PropDefaultType
editor *
Editor | null

The Tiptap editor instance, usually created with `useEditor`. Renders nothing until the editor is ready.

disabled
boolean

Dims the content area and blocks pointer events. Disable the editor itself with Tiptap's `editable` option.

Toolbar

PropDefaultType
variant 'fixed'
'fixed' | 'sticky' | 'floating'

`fixed` sits above the content, `sticky` follows the scroll position, `floating` is a panel for use inside a bubble menu.

stickyOffset '0px'
string

Distance from the top of the scroll container, used by the `sticky` variant.

Content

PropDefaultType
editorProps
EditorProps

ProseMirror view props forwarded to Tiptap's `EditorContent`.

ButtonControl

PropDefaultType
label *
string

Accessible name for the button, also shown as its tooltip.

icon *
React.ReactNode

The icon rendered inside the button.

createBooleanControl

PropDefaultType
label *
string

Accessible name for the control, also shown as its tooltip.

icon *
React.ElementType

The icon component rendered inside the button.

command *
(editor: Editor) => void

Runs when the control is clicked, usually a chained Tiptap command.

getVariant
(editor: Editor) => IconButtonProps['variant']

Derives the button variant from the editor state, typically `subtle` when the mark is active.

isDisabled
(editor: Editor) => boolean

Disables the control based on the editor state, for example when the command can't run.

getProps
(editor: Editor) => Record<string, any>

Props derived from the editor state. Takes precedence over `getVariant`.

createSelectControl

PropDefaultType
label *
string

Accessible name for the select, also shown as its tooltip.

options *
SelectOption[]

The selectable options, each with a `value`, `label` and optional `icon`.

getValue *
(editor: Editor) => string

Derives the selected option value from the editor state.

command *
(editor: Editor, value: string) => void

Runs when an option is selected.

placeholder 'Select'
string

Shown when the current editor state matches none of the options.

renderValue
(value: string, option?: SelectOption) => React.ReactNode

Customizes how the selected option is rendered in the trigger.

width
string

Width of the select trigger.

createSwatchControl

PropDefaultType
label *
string

Accessible name for the control, also shown as its tooltip.

swatches *
SwatchOption[]

The colors shown in the popover, each with a `value` and a `color`.

getValue *
(editor: Editor) => string

Derives the active color from the editor state.

command *
(editor: Editor, value: string) => void

Runs when a swatch is picked.

showRemove false
boolean

Adds a close button to the popover that clears the color.

onRemove
(editor: Editor) => void

Runs when the remove button is clicked.

icon
React.ElementType

Icon rendered above the color bar in the trigger.