Edits content as blocks. Text blocks (paragraphs, headings, lists, quotes, code, tables, images, dividers) are edited inline; page blocks such as a hero or a pricing table render as the very component the site uses, configured in an inspector generated from the block's schema. Reads and writes VamosDoc, so the site renders the same content with @vamos-ui/react/render and never loads the editor. A compact RichTextEditor covers notes, comments and announcements.
Harbor for clinics — landing page
DraftInstallation
The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";
Usage
import { BlockEditor, RichTextEditor } from "@vamos-ui/react/editor";
import { siteBlocks } from "@/blocks/registry"; // createRegistry([heroBlock, pricingBlock, …])
// Posts and pages: text + page blocks, "/" menu, drag handle, inserter
<BlockEditor value={doc} onChange={(next) => save(next)} registry={siteBlocks} variant="document" />
// A note field in a form (RichDoc in and out)
<Field label="Summary"><RichTextEditor value={note} onChange={setNote} size="sm" /></Field>
// The site (server component, no editor code)
import { renderDoc } from "@vamos-ui/react/render";
renderDoc(doc, { registry: siteBlocks, locale: "en" });Keyboard: Esc selects the current block. In block selection, ↑/↓ move between blocks, Alt+Shift+↑/↓ moves the block, ⌘/Ctrl+Shift+D duplicates, Alt+Shift+Z deletes, Enter goes back to the text. Insertions, moves and deletes are announced.
Markdown shortcuts while typing: # to ####, - or *, 1., >, ``` and ---.
Storage: image blocks are { type: "image", props: { src, alt, width, height, caption, assetId, focal } }; tables are table → table-row → table-cell (props.header, colspan, rowspan) with inline content.
Unknown block types (a disabled module) show a placeholder and are saved back exactly as they were loaded.
Examples
Rich-text field in a form (CRM activity)
RichTextEditor reads the surrounding Field: label, hint, error and required are wired to the text area. Value is a RichDoc, the format of f.rich() fields.
Your own block, end to end
defineBlock once: the component renders on the site and inside the editor; the zod schema drives validation and the inspector; preview props fill a new block. variant="canvas" runs blocks edge to edge, canvasTheme puts the canvas in the site's accent.
SchemaForm on its own
The inspector engine: widgets come from f.* metadata (text, toggle, nested group, sortable list with keyboard move buttons). Validation issues land on the field.
Plans 3
{
"title": "Simple *pricing*",
"plans": [
{
"name": "Solo",
"price": "$29",
"period": "month",
"features": [
"1 practitioner",
"SMS reminders"
]
},
{
"name": "Clinic",
"price": "$89",
"period": "month",
"features": [
"Up to 8 chairs",
"Waitlist autofill",
"Online payments"
],
"featured": true
},
{
"name": "Group",
"price": "$240",
"period": "month",
"features": [
"Unlimited locations",
"SSO and audit log"
]
}
]
}Restricted block set (CRM quote)
allowedBlocks limits what the / menu and the inserter offer. Tables use Tab to move between cells.
Editor and site output side by side
The same VamosDoc through renderDoc from @vamos-ui/react/render — RSC-safe and free of Tiptap.
Editor
renderDoc — site output
Opening our Kadıköy clinic
From 2 December we see patients six days a week at Moda Caddesi 41. Existing bookings move automatically; nobody needs to call us.
Good to know
- Is there parking?
Yes, two free spaces in front of the building.
Markdown and Word paste
Markdown becomes headings, lists, quotes, code and tables; Word and Google Docs HTML is cleaned of styles before parsing; a bare URL becomes a link.
Copy this, then paste below
## Spring opening hours We now open **an hour earlier** on weekdays. - Monday to Friday: 08:00–19:00 - Saturday: 09:00–14:00 > Bookings made before 1 March keep their time.
Read-only document with outline (HR policy)
readOnly keeps the text selectable and hides every editing control. The outline lists headings with word count and reading time.
API
BlockEditor
| Prop | Type | Default |
|---|---|---|
value / defaultValueContent. Doc-level fields (seo, layout, locales) are kept; blocks and meta are written by the editor. | VamosDoc | — |
onChangeDebounced 300 ms and flushed on blur. The editor's own state is the truth while typing. | (doc: VamosDoc, info: { wordCount; changedBlockIds }) => void | — |
registryPage blocks (createRegistry). The same registry the site renders with. Default: text blocks only. | BlockRegistry | — |
variantReading column (~720px), full-width page builder, or compact field (no inserter or drag handle). | "document" | "canvas" | "rich-text" | "document" |
allowedBlocksBlock types offered in the / menu and inserter: paragraph, heading, list, quote, code, divider, table, image and registry types. | string[] | ({ parentType }) => string[] | — |
patternsReady-made groups of blocks ({ id, title, category, doc }), shown in the inserter's Patterns tab. | BlockPattern[] | — |
placeholderEmpty-editor text (dictionary key blockEditor.placeholder). | string | "Type / to add a block" |
readOnlyDisplay only: no toolbar, menus, handle or inserter. | boolean | false |
media / uploadImagePasted and dropped images upload through here. Default: the VamosProvider media adapter. | MediaAdapter / (file, { signal }) => Promise<{ src; alt?; width?; height?; assetId? }> | — |
linkSearchInternal page suggestions in the link editor (⌘/Ctrl+K). | (q) => Promise<{ title; href; type }[]> | — |
canvasThemeSite theme axes on the canvas, so blocks look as they will live. The canvas is always solid, also inside a glass admin. | { accent?; font?; density?; theme? } | — |
langContent language: lang attribute (spellcheck, hyphenation), word counting and anchor slugs. | string | value.lang → UI locale |
editorRef{ focus, insertBlock(type, props?), getDoc, undo, redo, editor }. editor is the Tiptap instance. | Ref<BlockEditorHandle> | — |
size / minHeight / maxHeight / toolbarRich-text variant sizing and the fixed toolbar. | "sm" | "md" / CSS length / boolean | — |
aria-label / aria-labelledbyAccessible name of the text area. | string | "Content" |
labelsEvery built-in string (namespace blockEditor). | Partial<BlockEditorLabels> | — |
childrenYour composition of parts. Default: content, bubble menu, / menu, drag handle and inserter (toolbar for rich-text). | ReactNode | — |
Parts
| Prop | Type | Default |
|---|---|---|
BlockEditorContentThe canvas: contenteditable with role=textbox, aria-multiline, lang and Field wiring. | HTMLAttributes<div> | — |
BlockEditorBubbleMenurole=toolbar on a text selection: turn into, bold, italic, underline, strike, code, highlight, link. Arrow keys move, Alt+F10 focuses, Esc returns. | { children? } | — |
BlockEditorToolbarFixed toolbar (rich-text): marks, lists, quote, link, undo/redo. scripts adds sub/superscript. | { scripts?; children? } | — |
BlockEditorSlashMenu/ inserter. cmdk listbox driven from the text: arrows, Enter or Tab inserts, Esc closes. Search folds Turkish İ/ı. | { className? } | — |
BlockEditorDragHandleGutter + (insert below) and grip (drag, accent drop line) with a block menu: duplicate, move, turn into, copy link, hide, delete. | { className? } | — |
BlockInserter+ popover: Blocks | Patterns, cmdk search, live mini preview of the highlighted block. | { trigger?: ReactNode | null } | — |
BlockInspectorSettings of the selected page block from its schema: variant, props, anchor, hide. Images: alt text, decorative, caption. | { widgets?; onShowDocument?; documentLabel? } | — |
BlockEditorOutlineHeading outline, word count, reading time, warnings for skipped levels, several H1s and images without alt. | { stats? } | — |
RichTextEditor
| Prop | Type | Default |
|---|---|---|
value / defaultValue / onChangef.rich() value: paragraphs, h2–h4, lists, quotes, code, rules. onChange is debounced. | RichDoc | — |
id / invalid / required / aria-describedbyUsually set by the surrounding Field. | — | — |
sizeText size and padding. | "sm" | "md" | "md" |
minHeight / maxHeightThe text area scrolls past maxHeight. | CSS length | "7rem" / none |
toolbarFixed toolbar above the text. | boolean | true |
placeholder / readOnly / labels / autoFocusAs on BlockEditor. | — | — |
SchemaForm
| Prop | Type | Default |
|---|---|---|
schemaUsually a block's schema built with f.*. | z.ZodObject | — |
value / onChangeProps object (localized fields edit the locale entry). | Record<string, unknown> | — |
fieldsHints; group is content, style or advanced. | Record<key, { widget?; label?; help?; placeholder?; group?; hidden?; when?(v) }> | — |
widgetsCustom widgets by name (receive id, value, onChange, schema, meta, invalid, locale). | Record<string, WidgetComponent> | — |
errorsShown under the matching fields. | ZodError | { path; message }[] | — |
layoutStacked, or two columns in a dialog. | "sidebar" | "dialog" | "sidebar" |
variant / locale / disabledHide fields of other variants; locale edited in Localized<T>; read-only. | string / string / boolean | — |
Converters (pure)
| Prop | Type | Default |
|---|---|---|
toProseMirror(doc, registry?)Migrates the doc and component blocks to current versions first. Unknown types are carried untouched. | VamosDoc → JSONContent | — |
fromProseMirror(json, { base?, lang?, meta? })Stable ids (missing or duplicate ones replaced), merged runs, derived meta (wordCount, readingTime, toc). | JSONContent → VamosDoc | — |
richDocToProseMirror / proseMirrorToRichDocFor f.rich() fields; headings clamp to h2–h4. | RichDoc ↔ JSONContent | — |
markdownToBlocks / cleanPastedHtmlPaste conversion, usable on the server for imports. | string → BlockNode[] / string | — |