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.
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.jsonRTE 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>
)
}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",
]}
/>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"
}| Field | Type | Default | Description |
|---|---|---|---|
| runs | RTERun[] | — | 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. |
Adjacent runs with identical styling are merged automatically, so a document only ever has as many runs as there are distinct style spans.
| Field | Type | Default | Description |
|---|---|---|---|
| id | string | — | Stable identity for the run, used as its React key. |
| text | string | — | The run's text content. A run whose text is exactly "\n" renders as a line break. |
| fontFamily | string | "inherit" | CSS font-family value applied to this run. |
| fontSize | string | "inherit" | CSS font-size value applied to this run. |
| color | string | "inherit" | CSS color value applied to this run's text. |
| markerColor | string | "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. |
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"
}
]
}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.
Root provider. Owns document state, selection, pending style, and history.
Extends React.ComponentProps<"div"> (minus defaultValue, which is redefined below)
| Prop | Type | Default | Description |
|---|---|---|---|
| value | RTEDocument | — | Controlled document value. Pass together with onValueChange. |
| defaultValue | RTEDocument | — | Initial document value for uncontrolled usage. |
| onValueChange | (document: RTEDocument) => void | — | Called with the new document every time it changes. |
| historyLimit | number | 100 | Maximum number of undo/redo steps kept in history. |
| maxLength | number | — | Maximum number of characters allowed in the document. Typing, paste, and Enter are all rejected once the limit is reached. Leave unset for no limit. |
Toolbar container. Accepts children or a toolbar config array.
Extends React.ComponentProps<"div">
| Prop | Type | Default | Description |
|---|---|---|---|
| toolbar | RTEToolbarItem[] — "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. |
The contentEditable surface where the document is edited.
Extends React.ComponentProps<"div"> (minus onChange)
| Prop | Type | Default | Description |
|---|---|---|---|
| placeholder | string | — | Placeholder text shown via CSS when the document is empty. |
Divider between toolbar groups.
Extends React.ComponentProps<typeof Separator> (shadcn/ui Separator)
| Prop | Type | Default | Description |
|---|---|---|---|
| orientation | "horizontal" | "vertical" | "vertical" | Forwarded to the underlying shadcn Separator. |
Font family select.
Extends React.ComponentProps<typeof Select> (minus value, onValueChange)
| Prop | Type | Default | Description |
|---|---|---|---|
| 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. |
Font size select.
Extends React.ComponentProps<typeof Select> (minus value, onValueChange)
| Prop | Type | Default | Description |
|---|---|---|---|
| options | { label: string; value: string }[] | RTE_FONT_SIZE_OPTIONS (Small, Default, Medium, Large, X-Large) | Overrides the list of selectable font sizes. |
Text color picker: a native color-input square plus an RGB/hex text field.
Extends React.ComponentProps<typeof PopoverContent>
No component-specific props.
Highlight/marker color picker, same UI pattern as RTEColorSelect.
Extends React.ComponentProps<typeof PopoverContent>
No component-specific props.
Bold, italic, underline, strikethrough, overline toggle group.
Extends React.ComponentProps<typeof ToggleGroup> (minus type, value, onValueChange, defaultValue)
| Prop | Type | Default | Description |
|---|---|---|---|
| isBoldActive | boolean | true | Show the bold toggle. |
| isItalicActive | boolean | true | Show the italic toggle. |
| isUnderlineActive | boolean | true | Show the underline toggle. |
| isStrikethroughActive | boolean | false | Show the strikethrough toggle. |
| isOverlineActive | boolean | false | Show the overline toggle. |
Bullet and numbered list toggle group.
Extends React.ComponentProps<typeof ToggleGroup> (minus type, value, onValueChange, defaultValue)
| Prop | Type | Default | Description |
|---|---|---|---|
| isBulletActive | boolean | true | Show the bullet-list toggle. |
| isNumberActive | boolean | true | Show the numbered-list toggle. |
Left, center, right, justify alignment toggle group.
Extends React.ComponentProps<typeof ToggleGroup> (minus type, value, onValueChange, defaultValue)
| Prop | Type | Default | Description |
|---|---|---|---|
| isLeftActive | boolean | true | Show the align-left toggle. |
| isCenterActive | boolean | true | Show the align-center toggle. |
| isRightActive | boolean | true | Show the align-right toggle. |
| isJustifyActive | boolean | true | Show the justify toggle. |
Undo button. Disabled automatically when there is nothing to undo.
Extends React.ComponentProps<typeof Button> (minus onClick, disabled)
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | ButtonVariant | "outline" | Forwarded to the underlying shadcn Button. |
| size | ButtonSize | "sm" | Forwarded to the underlying shadcn Button. |
Redo button. Disabled automatically when there is nothing to redo.
Extends React.ComponentProps<typeof Button> (minus onClick, disabled)
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | ButtonVariant | "outline" | Forwarded to the underlying shadcn Button. |
| size | ButtonSize | "sm" | Forwarded to the underlying shadcn Button. |
Displays a "current/max" character counter, or just the count when maxLength is unset.
Extends React.ComponentProps<"span">
No component-specific props.
Convert the JSON document to another format whenever you need it.
| Function | Description |
|---|---|
| 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. |