Rich Text Editor

Documentation

Headless, shadcn-native rich text editor for React. The document is stored as JSON internally and can be serialized to HTML, JSON, or plain text on demand.

Installation

RTE is not published to npm. It is distributed as copy-paste source through a shadcn registry, so it lives directly in your project and can be edited freely.

npx shadcn add https://rte-shadcn.vercel.app/r/rte.json

Usage

RTE is a compound component. Compose only the pieces you need inside <RTE>.

import {
  RTE,
  RTEHeader,
  RTEContent,
  RTEFooter,
  RTESeparator,
  RTEFormatToggle,
  RTEListToggle,
  RTEAlignToggle,
  RTEFontSelect,
  RTEFontSizeSelect,
  RTEColorSelect,
  RTEMarkerSelect,
  RTEUndo,
  RTERedo,
  RTECharacterCount,
} from "@/components/rte"

export function Editor() {
  return (
    <RTE historyLimit={50} maxLength={500}>
      <RTEHeader>
        <RTEUndo />
        <RTERedo />
        <RTESeparator />
        <RTEFontSelect />
        <RTEFontSizeSelect />
        <RTEColorSelect />
        <RTEMarkerSelect />
        <RTESeparator />
        <RTEFormatToggle />
        <RTESeparator />
        <RTEListToggle />
        <RTESeparator />
        <RTEAlignToggle />
      </RTEHeader>
      <RTEContent placeholder="Start typing..." />
      <RTEFooter>
        <RTECharacterCount />
      </RTEFooter>
    </RTE>
  )
}

Toolbar config array

As an alternative to composing toolbar children by hand, RTEHeader accepts a toolbar prop. If both are provided, JSX children take precedence.

<RTEHeader
  toolbar={[
    "undo",
    "redo",
    "separator",
    "fontFamily",
    "fontSize",
    "separator",
    "bold",
    "italic",
    "underline",
  ]}
/>

Document model

RTE stores its content as a single JSON value, not a DOM tree. A document is a flat, ordered list of styled text runs, and each run carries its own align — so every line can have a different alignment. This is what you get from value/defaultValue/onValueChange on <RTE>, and it's exactly what serializeToJson prints.

type RTEDocument = {
  runs: RTERun[]
}

type RTERun = {
  id: string
  text: string
  fontFamily: string
  fontSize: string
  color: string
  markerColor: string
  format: string[] // e.g. ["bold", "italic"]
  align: "left" | "center" | "right" | "justify"
}

RTEDocument

FieldTypeDefaultDescription
runsRTERun[]A flat, ordered list of styled text spans. Adjacent runs with identical styling (including align) are merged automatically, so this list is always minimal. A run whose text is exactly "\n" marks a line break.

RTERun

Adjacent runs with identical styling are merged automatically, so a document only ever has as many runs as there are distinct style spans.

FieldTypeDefaultDescription
idstringStable identity for the run, used as its React key.
textstringThe run's text content. A run whose text is exactly "\n" renders as a line break.
fontFamilystring"inherit"CSS font-family value applied to this run.
fontSizestring"inherit"CSS font-size value applied to this run.
colorstring"inherit"CSS color value applied to this run's text.
markerColorstring"transparent"CSS background-color value used for the highlight/marker.
format("bold" | "italic" | "underline" | "strikethrough" | "overline")[][]Character-level marks active on this run.
align"left" | "center" | "right" | "justify""left"Alignment of the line this run belongs to. RTEAlignToggle applies it to every run on the current line (or every line touched by the selection), so each line can have its own alignment.

Example

A centered title line followed by a left-aligned body line — each line's runs carry their own align:

{
  "runs": [
    {
      "id": "run-1-a1b2c3",
      "text": "Centered title",
      "fontFamily": "inherit",
      "fontSize": "inherit",
      "color": "inherit",
      "markerColor": "transparent",
      "format": [],
      "align": "center"
    },
    {
      "id": "run-2-d4e5f6",
      "text": "\n",
      "fontFamily": "inherit",
      "fontSize": "inherit",
      "color": "inherit",
      "markerColor": "transparent",
      "format": [],
      "align": "center"
    },
    {
      "id": "run-3-g7h8i9",
      "text": "Left-aligned body text.",
      "fontFamily": "inherit",
      "fontSize": "inherit",
      "color": "inherit",
      "markerColor": "transparent",
      "format": [],
      "align": "left"
    }
  ]
}

Components & props

Every piece is a separate component that reads shared state from <RTE>'s context. Each also forwards the props of the underlying element or shadcn primitive it wraps (className, style, etc.) — only the props specific to that component are listed below.

RTE

Root provider. Owns document state, selection, pending style, and history.

Extends React.ComponentProps<"div"> (minus defaultValue, which is redefined below)

PropTypeDefaultDescription
valueRTEDocumentControlled document value. Pass together with onValueChange.
defaultValueRTEDocumentInitial document value for uncontrolled usage.
onValueChange(document: RTEDocument) => voidCalled with the new document every time it changes.
historyLimitnumber100Maximum number of undo/redo steps kept in history.
maxLengthnumberMaximum number of characters allowed in the document. Typing, paste, and Enter are all rejected once the limit is reached. Leave unset for no limit.

RTEHeader

Toolbar container. Accepts children or a toolbar config array.

Extends React.ComponentProps<"div">

PropTypeDefaultDescription
toolbarRTEToolbarItem[] — "undo" | "redo" | "fontFamily" | "fontSize" | "color" | "marker" | "format" | "list" | "align" | "separator"Renders toolbar items from a config array instead of composing them as JSX. Ignored if children are provided — children always win.

RTEContent

The contentEditable surface where the document is edited.

Extends React.ComponentProps<"div"> (minus onChange)

PropTypeDefaultDescription
placeholderstringPlaceholder text shown via CSS when the document is empty.

RTEFooter

Footer container, typically used to hold the character count.

Extends React.ComponentProps<"div">

No component-specific props.

RTESeparator

Divider between toolbar groups.

Extends React.ComponentProps<typeof Separator> (shadcn/ui Separator)

PropTypeDefaultDescription
orientation"horizontal" | "vertical""vertical"Forwarded to the underlying shadcn Separator.

RTEFontSelect

Font family select.

Extends React.ComponentProps<typeof Select> (minus value, onValueChange)

PropTypeDefaultDescription
options{ label: string; value: string }[]RTE_FONT_OPTIONS (13 built-in fonts)Overrides the list of selectable fonts. Each value is a CSS font-family string.

RTEFontSizeSelect

Font size select.

Extends React.ComponentProps<typeof Select> (minus value, onValueChange)

PropTypeDefaultDescription
options{ label: string; value: string }[]RTE_FONT_SIZE_OPTIONS (Small, Default, Medium, Large, X-Large)Overrides the list of selectable font sizes.

RTEColorSelect

Text color picker: a native color-input square plus an RGB/hex text field.

Extends React.ComponentProps<typeof PopoverContent>

No component-specific props.

RTEMarkerSelect

Highlight/marker color picker, same UI pattern as RTEColorSelect.

Extends React.ComponentProps<typeof PopoverContent>

No component-specific props.

RTEFormatToggle

Bold, italic, underline, strikethrough, overline toggle group.

Extends React.ComponentProps<typeof ToggleGroup> (minus type, value, onValueChange, defaultValue)

PropTypeDefaultDescription
isBoldActivebooleantrueShow the bold toggle.
isItalicActivebooleantrueShow the italic toggle.
isUnderlineActivebooleantrueShow the underline toggle.
isStrikethroughActivebooleanfalseShow the strikethrough toggle.
isOverlineActivebooleanfalseShow the overline toggle.

RTEListToggle

Bullet and numbered list toggle group.

Extends React.ComponentProps<typeof ToggleGroup> (minus type, value, onValueChange, defaultValue)

PropTypeDefaultDescription
isBulletActivebooleantrueShow the bullet-list toggle.
isNumberActivebooleantrueShow the numbered-list toggle.

RTEAlignToggle

Left, center, right, justify alignment toggle group.

Extends React.ComponentProps<typeof ToggleGroup> (minus type, value, onValueChange, defaultValue)

PropTypeDefaultDescription
isLeftActivebooleantrueShow the align-left toggle.
isCenterActivebooleantrueShow the align-center toggle.
isRightActivebooleantrueShow the align-right toggle.
isJustifyActivebooleantrueShow the justify toggle.

RTEUndo

Undo button. Disabled automatically when there is nothing to undo.

Extends React.ComponentProps<typeof Button> (minus onClick, disabled)

PropTypeDefaultDescription
variantButtonVariant"outline"Forwarded to the underlying shadcn Button.
sizeButtonSize"sm"Forwarded to the underlying shadcn Button.

RTERedo

Redo button. Disabled automatically when there is nothing to redo.

Extends React.ComponentProps<typeof Button> (minus onClick, disabled)

PropTypeDefaultDescription
variantButtonVariant"outline"Forwarded to the underlying shadcn Button.
sizeButtonSize"sm"Forwarded to the underlying shadcn Button.

RTECharacterCount

Displays a "current/max" character counter, or just the count when maxLength is unset.

Extends React.ComponentProps<"span">

No component-specific props.

Output helpers

Convert the JSON document to another format whenever you need it.

FunctionDescription
serializeToJson(document)Serializes the document to a JSON string.
serializeToHtml(document)Serializes the document to an HTML string.
serializeToPlainText(document)Serializes the document to plain text.