Editor
The editor molecule, on tiptap: an EditorProvider core, the RichTextKit extension bundle, the field editor in three variants, and the full document editor with block drag, tables, mentions and slash commands.
Playground
The field editor. Pick it with the controls beside it — the ghost variant stands on its own text; select a run of it to raise the floating bar.
450 characters remaining
<Editor />Installation
npx shadcn@latest add https://espresso-base-ui.vercel.app/r/editor.jsonUsage
import { Editor, RichTextEditor } from "@/components/ui/editor"// a field: comments, descriptions
<Editor variant="subtle" limit={450} onChange={(html) => save(html)} />
// a document
<RichTextEditor content={article} onChange={(html) => save(html)} />Both are compositions over the same core. For a custom editor, compose it yourself the same way — the provider owns the tiptap instance, RichTextKit is one configurable extension bundle where false drops a feature and an object configures it:
import { EditorContent, EditorProvider, RichTextKit } from "@/components/ui/editor"
<EditorProvider
extensions={[RichTextKit.configure({ table: false, mention: false, limit: 280 })]}
content={html}
onUpdate={(html) => save(html)}
>
{(editor) => (
<>
<MyToolbar editor={editor} />
<EditorContent editor={editor} />
</>
)}
</EditorProvider>Variants
variant picks the field's surface: outline draws a 1px rule that darkens on hover and focus; subtle sits on a filled surface and turns white focused; ghost is the text alone — a selection washes amber and the full bar floats over it.
450 characters remaining
450 characters remaining
<Editor variant="outline" />
<Editor variant="subtle" />
<Editor variant="ghost" toolbar="none" content={content} />Toolbar
toolbar places the field's bar: top is the full bar with the character count and corner resizer, bottom is the comment bar with Discard and Comment (wired to onDiscard / onComment), and none leaves the text alone.
<Editor toolbar="bottom" onComment={(html) => addComment(html)} />Character limit
limit caps the text (450 by default) at the ProseMirror level — typing, pasting or dropping — and the foot counts down what is left.
80 characters remaining
<Editor limit={80} />Rich text editor
The document editor takes the whole kit: the fixed bar (block styles, fonts and sizes, colours, lists, alignment, tables with the size picker, the emoji picker, media, highlighter), Notion-style block drag handles, table row/column grips, a floating bar over selections, @ mentions and / slash commands, callouts, toggle lists, columns, attachments, audio and embeds.
<RichTextEditor content={article} />editable={false} renders the document alone — the bar and handles are omitted — which suits rendering saved content. The component fills its parent; give it a bounded height for an internal scroll, or none to lay the document out full-length.
<RichTextEditor content={article} editable={false} />API reference
Editor
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "outline" | "subtle" | "ghost" | "outline" | Surface — ruled outline, filled subtle, or bare ghost with the floating selection bar. |
| toolbar | "top" | "bottom" | "none" | "top" | The full bar on top with count and resizer, the comment bar at the bottom, or none. |
| content | string | "" | The HTML it opens with. |
| placeholder | string | "Type something..." | Shown while the document is empty, even disabled. |
| limit | number | 450 | The most characters it holds. |
| disabled | boolean | false | Read-only, drawn at half opacity. |
| onChange / onComment / onDiscard | (html?: string) => void | – | Edits, the comment bar's Comment (then clears), and its Discard. |
RichTextEditor
| Prop | Type | Default | Description |
|---|---|---|---|
| content | string | "" | The HTML it opens with. |
| editable | boolean | true | Read-only hides the bar, the grips and the table controls. |
| placeholder | string | "Type something, ..." | Shown while the document is empty. |
| onChange | (html: string) => void | – | Fires with the document HTML on every edit. |
EditorProvider takes extensions, content, editable, onUpdate, onTransaction and a render-prop child; useCurrentEditor() reads the instance from context. RichTextKit accepts a flag per feature — heading, table, taskList, details, columns, callout, attachment, audio, embed, video, mention, slashCommands, fontFamily, fontSize, lineHeight, subscript, superscript, placeholder and limit.
Styling hooks
The field carries data-slot="editor" with data-variant / data-disabled; the document data-slot="rich-text-editor" (and data-readonly), its bar data-slot="rich-text-editor-toolbar". The editorVariants cva and both icon sets are exported.
import { editorVariants } from "@/components/ui/editor"