Skip to content

Repository files navigation

RTE — Rich Text Editor

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.

Why

  • 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 add copies the source directly into your project, so you own and can edit every line.

Installation

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

This copies the RTE* components into components/rte in your project.

Usage

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.

Document model

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" }
  ]
}

Output helpers

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.

RTE props

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.

Components

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.

Monorepo layout

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)

Development

npm install
npm run dev:website   # starts apps/website on http://localhost:3000

Package manager: npm (not pnpm/yarn).

About

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages