Headless, shadcn-native rich text editor for React. Built entirely from scratch (no ProseMirror, Lexical, or Slate), styled with shadcn/ui primitives, and distributed as copy-paste source through a shadcn registry rather than an npm package.
- Site & docs: https://rte-shadcn.vercel.app
- Playground: https://rte-shadcn.vercel.app/playground
- Repo: https://github.com/FlyingTurkman/rich-text-editor
- Compound components.
RTE,RTEHeader,RTEContent,RTEFontSelect,RTEColorSelect, etc. — every toolbar feature is its own component, composed the way shadcn components are composed. - JSON document model. The editor's content is a single JSON value (
RTEDocument), not a hidden DOM tree. Serialize it to JSON, HTML, or plain text on demand. - Not an npm package.
npx shadcn addcopies the source directly into your project, so you own and can edit every line.
npx shadcn add https://rte-shadcn.vercel.app/r/rte.jsonThis copies the RTE* components into components/rte in your project.
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 also accepts a toolbar config array:
<RTEHeader
toolbar={["undo", "redo", "separator", "fontFamily", "fontSize", "separator", "bold", "italic", "underline"]}
/>If both children and toolbar are given, children wins.
RTE stores its content as one JSON value: a flat, ordered list of styled text runs. Each run carries its own align, so every line can have a different alignment. This is exactly what you get from value / defaultValue / onValueChange on <RTE>, and what serializeToJson prints.
type RTEDocument = {
runs: RTERun[]
}
type RTERun = {
id: string
text: string
fontFamily: string // default "inherit"
fontSize: string // default "inherit"
color: string // default "inherit"
markerColor: string // default "transparent"
format: string[] // "bold" | "italic" | "underline" | "strikethrough" | "overline"
align: "left" | "center" | "right" | "justify" // default "left"
}Adjacent runs with identical styling (including align) are merged automatically, so a document only ever has as many runs as there are distinct style spans. A run whose text is exactly "\n" renders as a line break.
RTEAlignToggle applies the chosen alignment to every run on the current line (or every line touched by the selection), so lines are aligned independently — there is still no paragraph/block object, alignment simply travels with each run.
Example — a centered title line followed by a left-aligned body line:
{
"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" }
]
}| Function | Description |
|---|---|
serializeToJson(document) |
Serializes the document to a JSON string (its native shape). |
serializeToHtml(document) |
Serializes the document to an HTML string, one <div style="text-align: ..."> per line. |
serializeToPlainText(document) |
Serializes the document to plain text. |
| Prop | Type | Default | Description |
|---|---|---|---|
value |
RTEDocument |
— | Controlled document value. |
defaultValue |
RTEDocument |
— | Initial document value for uncontrolled usage. |
onValueChange |
(document: RTEDocument) => void |
— | Called with the new document on every change. |
historyLimit |
number |
100 |
Maximum undo/redo steps kept in history. |
maxLength |
number |
— | Maximum character count. Typing, paste, and Enter are rejected past this limit. |
| Component | Description |
|---|---|
RTE |
Root provider. Owns document state, selection, pending style, and history. |
RTEHeader |
Toolbar container. Accepts children or a toolbar config array. |
RTEContent |
The contentEditable surface where the document is edited. |
RTEFooter |
Footer container, typically used for the character count. |
RTESeparator |
Divider between toolbar groups. |
RTEFormatToggle |
Bold, italic, underline, strikethrough, overline toggle group. |
RTEListToggle |
Bullet and numbered list toggle group. |
RTEAlignToggle |
Left, center, right, justify alignment toggle group. |
RTEFontSelect |
Font family select (13 built-in fonts). |
RTEFontSizeSelect |
Font size select. |
RTEColorSelect |
Text color picker (native color input + RGB/hex text field). |
RTEMarkerSelect |
Highlight/marker color picker, same pattern as RTEColorSelect. |
RTEUndo / RTERedo |
Undo / redo buttons, auto-disabled when there's nothing to undo/redo. |
RTECharacterCount |
Displays a current/max character counter. |
Every component forwards the props of the underlying element or shadcn primitive it wraps (className, style, etc.) in addition to its own. Full prop tables for each component live in the docs page.
apps/website/ Next.js site (home, /docs, /playground) and the registry source
registry/rte/ The RTE component source — what npx shadcn add copies
registry.json shadcn registry manifest
public/r/ Built registry output (npx shadcn build)
npm install
npm run dev:website # starts apps/website on http://localhost:3000Package manager: npm (not pnpm/yarn).