Rich Text Editor
alphav0.3.0Plate-powered WYSIWYG editor and read-only viewer for long-form articles — code blocks, captioned images, floating toolbar, HTML export.
Context
First Plate (platejs) component in pro-ui. Two exports from one folder: `<RichTextEditor>` is a 'use client' editor (~165KB gzip after v0.2 additions) with a fixed top toolbar (marks / headings / lists / blockquote / link / image / table / code-block / font-family / font-size / color) PLUS a selection-anchored floating toolbar (marks + link). `<RichTextViewer>` is server-renderable via `platejs/static` (~32KB gzip). Both consume the same Plate `Value` JSON shape. Storage format is JSON, not HTML — Plate's docs warn against HTML round-trips. v0.2 adds: lowlight code-block syntax highlighting (15 languages registered, tokens themed via chart-1..5 palette in globals.css); per-image resize handle + inline caption editor (width stored as %); floating toolbar (selection-anchored, anchored via @floating-ui/react); HTML serialization escape hatch via `serializeRichTextToHtml(value)` for RSS / email / OG-tag export boundaries.
Installation
pnpm dlx shadcn@latest init"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}pnpm dlx shadcn@latest add @ilinxa/rich-text-editorAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/rich-text-editor-fixturesPreview
Press Cmd/Ctrl+S to fire onSave. Select text to surface the floating toolbar.
Demo source
Usage
When to use
A WYSIWYG rich-text editor + read-only viewer for long-form article bodies — news, blog posts, doc pages, knowledge-base entries. Built on Plate (platejs), pro-ui's WYSIWYG substrate.
Two exports from one folder: RichTextEditor is a "use client" editor (~150KB gzip); RichTextVieweris server-renderable (~30KB gzip). Storage format is JSON — Plate's docs warn against HTML round-trips.
Editor
"use client"
import {
RichTextEditor,
type RichTextValue,
} from "@/components/rich-text-editor"
export function ArticleEditPage({ initial }: { initial: RichTextValue }) {
const [value, setValue] = useState(initial)
return (
<RichTextEditor
value={value}
onChange={setValue}
onSave={async (v) => {
await fetch("/api/articles/save", { method: "POST", body: JSON.stringify(v) })
}}
onImageUpload={async (file) => {
const fd = new FormData()
fd.append("file", file)
const res = await fetch("/api/upload", { method: "POST", body: fd })
return res.json() // { src, alt?, width?, height? }
}}
/>
)
}Viewer (RSC)
The viewer is a server component — no "use client" boundary needed. It uses platejs/static to render the JSON without instantiating an editor.
import { RichTextViewer } from "@/components/rich-text-editor"
export default async function NewsArticle({ params }: { params: { id: string } }) {
const article = await fetchArticle(params.id)
return (
<article>
<h1>{article.title}</h1>
<RichTextViewer value={article.body} />
</article>
)
}Composed with the news-domain article column
<article className="lg:col-span-8">
<h1>{article.title}</h1>
<ArticleMeta divider items={metaItems} />
<RichTextViewer value={article.body} />
<ShareBar targets={[...]} title={article.title} headingAs="h4" divider />
</article>Image upload contract
The component never talks to a backend directly. Pass onImageUpload — a function that takes a File and resolves to { src, alt?, width?, height? }. Without this prop, the editor falls back to a URL prompt.
Toolbar
Marks: bold, italic, underline, strikethrough, inline code, highlight, subscript, superscript. Blocks: H1–H3, blockquote, code block, horizontal rule. Lists: bullet, ordered. Insert: link, image, table. Styles: font family, font size, text color.
Notes
- Storage format is the Plate
Valuetype (an array of element nodes). Don't round-trip through HTML — keep JSON authoritative; serialize to HTML only at export boundaries (RSS, email, etc.). - The viewer renders bare HTML with Plate's data-slate-* attributes stripped — no editor instance, no client hooks. Safe in Server Components.
- Press
Cmd/Ctrl+Sto fireonSavewith the current document. OverrideautoFocusto focus the editor on mount. - The editor is
echo-guardedon thevalueprop: external updates flow in viasetValueon the editor without triggering an extraonChangeemit. - Toolbar buttons are
onMouseDown-driven so the editor selection isn't lost when clicking a button. - Plate ships a brand-recognizable suite of plugins under
@platejs/*. This component pins to v53. When bumping Plate, re-verify SSR, image upload, and the JSON shape.
Features
- Editor + Viewer split — same JSON shape, different bundle profiles
- Fixed top toolbar (marks, headings, lists, blockquote, link, image, table, code, font-family, font-size, color)
- Floating toolbar (selection-anchored marks + link insertion) — appears on text selection, anchored via @floating-ui/react virtual element
- Lowlight syntax highlighting on code blocks (15 languages: bash / css / diff / go / html / java / javascript / json / markdown / python / rust / shell / sql / typescript / xml / yaml; tokens themed via pro-ui chart palette)
- Image resize handle + inline caption editor (width stored as percentage, caption editable inline)
- HTML serialization escape hatch via serializeRichTextToHtml(value) — async, server-only, for export boundaries (RSS / email / OG tags)
- Image insertion via onImageUpload(file) Promise OR URL prompt fallback
- Tables (insert + keyboard-driven row/col ops via Plate's table plugin)
- Indent / outdent on paragraphs / headings / blockquotes / code blocks
- Controlled OR uncontrolled value (echo-guarded sync from external value prop)
- Cmd/Ctrl+S → onSave(value); platform-aware key descriptor in footer
- JSON-as-storage (not HTML round-trips per Plate's storage guidance)
- Read-only mode hides both toolbars; editor still selectable
- Pure server-renderable viewer via createStaticEditor + PlateStatic
- Tailwind v4 + signal-lime token integration; prose styling via @tailwindcss/typography