Rich Text Editor

A WYSIWYG editor for formatted prose: headings, bold, italic, underline, links and lists. Built on Tiptap for selection, undo and paste, with every pixel of UI owned as a Koala tv recipe.

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add rich-text-editor

Usage#

usage.tsxTSX
import {  RichTextEditor,  RichTextEditorToolbar,  RichTextEditorContent,} from "@/components/ui/rich-text-editor"export function Example() {  return (    <RichTextEditor      defaultValue="<p>Hello, world.</p>"      onChange={(html) => console.log(html)}    >      <RichTextEditorToolbar />      <RichTextEditorContent />    </RichTextEditor>  )}

Placeholder#

An empty editor shows muted ghost text. Set it with the placeholder prop.

A complete composer#

Everything together: a seeded document, the full toolbar, the on-selection bubble, a scrollable writing area, and a status bar that lives inside the editor surface: live word/character counts (tabular-nums, so they never reflow) and a save action with transient confirmation. The status bar is just a child that reads the editor via useRichTextEditor.

0 words · 0 characters

Selection bubble menu#

Drop in RichTextEditorBubbleMenu and a floating toolbar surfaces over the current selection. Double-click a word or drag across a phrase and it appears above it (Medium / Notion style). It pairs with the top toolbar or replaces it entirely. Its link control expands inline so the selection-driven bubble never dismisses while you type the URL.

Or go chromeless: omit the top toolbar and let the bubble be the only formatting UI.

Density#

Like the rest of Koala, the editor honors density. compact tightens the writing area for dense app surfaces. The toolbar sits outside that axis: its 8px padding is what keeps the controls' 12px corners concentric inside the 20px frame, and a format bar that shrank with form density would get hard to hit, so the controls hold 32px either way. Set it per instance or once with a DensityProvider.

Read-only#

Pass editable={false} and omit the toolbar to render stored content as styled prose, useful for previews and feeds.

Custom toolbar#

With no children the toolbar renders the essential controls. For a custom set, pass RichTextEditorButtons and reach the live editor with the useRichTextEditor hook (pair it with Tiptap's useEditorState to reflect active formatting).

Prose scale#

Inside the editor, blocks render on a document scale tuned for reading: the heading steps mirror Tailwind's @tailwindcss/typography prose-sm proportions, scaled to the 14px body (no plugin dependency). Headings run text-3xl → text-sm in the strong foreground, while body copy stays the muted secondary color, so a document reads calm with only its headings and emphasis lifted. These sizes are fixed by the component. For the raw scale they draw from, see the Typography foundation.

H1

text-3xl · 30px

Heading one

H2

text-xl · 20px

Heading two

H3

text-lg · 18px

Heading three

H4

text-sm · 14px

Heading four

Body

text-sm · 14px

Body copy reads in the secondary text color, so only headings and emphasis carry the strong foreground.

FAQ#

Rich text needs ProseMirror's document model for selection, undo, and paste, which no Radix primitive provides. Like Tooltip wrapping Tippy.js, it is the considered exception to Koala's Radix-first rule, with every pixel of UI still owned as a tv recipe.