richtext field
A richtext() field stores one RichTextDocument — an ordered array of
block-level nodes, each either an element ({ type, children }) or a text
leaf ({ text, ...marks }). Core only owns this structural contract; the
actual node catalogue (headings, bold, links, images, tables, …) is supplied
entirely by whichever editor adapter is configured on VexClientConfig’s
editor option. Without one, the admin Input renders a configuration
error instead of an editor.
@vexcms/richtext-plate is the built-in editor adapter, built on
Plate (platejs). It ships both the interactive
editor (admin panel) and a server-safe static renderer (frontend) behind one
plateEditor() call.
Config options
Section titled “Config options”| Option | Type | Default | Description |
|--------|------|---------|-------------|
| mediaCollectionSlug | MediaCollectionSlug | — | Media collection inline images upload into. Validated against VexConfig.mediaCollections at defineConfig() time, exactly like upload()’s to. Absent means the editor’s image feature is hidden entirely — no dropzone, no paste/drag upload, no “insert image” toolbar button — rather than rendering a feature that would fail every upload. |
| label | string | "" | Display label in the admin panel. Inferred from the field key by defineCollection when left empty. |
| required | boolean | false | When false, the field is wrapped in v.optional(…) in the schema. |
| defaultValue | RichTextDocument | [] | Pre-filled document shown when creating a new document. |
| description | string | — | Helper text shown below the field input. |
| admin.hidden | boolean | false | Hides the field from the admin edit form entirely. |
| admin.readOnly | boolean | false | Renders the editor as non-interactive. |
| admin.position | "main" \| "sidebar" | "main" | Column in the admin edit layout where this field appears. |
| admin.width | string | "100%" | CSS width of the field in the edit form. Meaningful as a child of row(). |
| admin.cellAlignment | "left" \| "center" \| "right" | "left" | Alignment used for this field’s column in the list-view table. |
| admin.condition | (props: { data, siblingData }) => boolean | — | Shows the field only while this returns true. See the Conditional fields & masks guide. |
| validate | (props) => Promise<void> \| void | — | Server-only async validation; value is typed as RichTextDocument. |
Schema output
Section titled “Schema output”Richtext documents are stored as v.array(v.any()) — Convex validators
cannot express the recursive element/leaf shape, so the element-level
contract (every entry is { type, children } or { text }) is enforced
only by the admin form’s Zod schema, not the database schema:
// Optional (required: false — default)body: v.optional(v.array(v.any()))
// Requiredsummary: v.array(v.any())import { defineCollection, richtext, text } from "@vexcms/core";
const posts = defineCollection({ slug: "posts", fields: { title: text({ required: true }),
// Plain body, no inline images body: richtext({ label: "Body", description: "Rich text content for the post.", }),
// Allows inline images, uploaded into the "images" media collection content: richtext({ label: "Content", mediaCollectionSlug: "images", required: true, }), },});The editor adapter
Section titled “The editor adapter”A richtext() field needs an editor adapter registered on the client
config’s editor option — it supplies both the interactive admin editor
component and the frontend renderer. Without it, every richtext field in the
admin panel shows a configuration error instead of markup.
import { defineConfig, defineCollection, richtext } from "@vexcms/core";import { plateEditor } from "@vexcms/richtext-plate";
export default defineConfig({ editor: plateEditor(), collections: [ defineCollection({ slug: "posts", fields: { body: richtext({ mediaCollectionSlug: "images" }), }, }), ],});plateEditor(options?)
Section titled “plateEditor(options?)”| Option | Type | Default | Description |
|--------|------|---------|-------------|
| features | VexEditorFeature[] \| ((defaults: VexEditorFeature[]) => VexEditorFeature[]) | defaultFeatures | Which node/mark features the editor enables. Pass an explicit array to replace the defaults, or a callback that filters/extends them. |
@vexcms/richtext-plate ships one VexEditorFeature per node/mark —
BoldFeature, ItalicFeature, UnderlineFeature, StrikethroughFeature,
CodeFeature, HeadingFeature, BlockquoteFeature, CodeBlockFeature,
ListFeature, LinkFeature, ImageFeature, HorizontalRuleFeature,
TableFeature — all enabled by default via defaultFeatures, importable
from @vexcms/richtext-plate/editor:
import { plateEditor } from "@vexcms/richtext-plate";import { BoldFeature, ItalicFeature, LinkFeature } from "@vexcms/richtext-plate/editor";
export default defineConfig({ // Only bold, italic, and links editor: plateEditor({ features: [BoldFeature, ItalicFeature, LinkFeature] }),});// Remove tables from the defaults, keep everything elseeditor: plateEditor({ features: (defaults) => defaults.filter((f) => f.key !== "table") }),Admin UI
Section titled “Admin UI”The editor renders inline in the document form — headings, marks (bold,
italic, underline, strikethrough, code), blockquotes, code blocks, ordered
and unordered lists, links, a horizontal rule, and tables, depending on
which features are enabled. When mediaCollectionSlug is set, the image
feature’s toolbar button, drag-and-drop, and paste-to-upload all push new
files into that media collection the same way an upload() field does;
when it is unset, the image feature doesn’t render at all.
Rendering on the frontend
Section titled “Rendering on the frontend”@vexcms/richtext-plate’s RichText component renders a saved
RichTextDocument on the frontend. It’s a static, server-safe renderer
(platejs/static’s PlateStatic, no browser APIs) — safe to use in React
Server Components:
import { RichText } from "@vexcms/richtext-plate/render";
export function PostBody({ body }: { body: RichTextDocument }) { return <RichText content={body} className="prose" />;}| Prop | Type | Description |
|------|------|--------------|
| content | RichTextDocument | The saved document to render. Renders nothing when empty. |
| className | string | Wrapper class, e.g. a Tailwind Typography prose class. |
| resolveMedia | (props) => … | Accepted for adapter parity with other render components; not currently supported by this static renderer (logs a warning if provided). |
Package exports
Section titled “Package exports”| Entry | Exports |
|-------|---------|
| @vexcms/richtext-plate | plateEditor, RichText, element types (ImageElement, LinkElement, TableElement, TableRowElement, TableCellElement), VexEditorFeature |
| @vexcms/richtext-plate/editor | plateEditor, every feature constant (BoldFeature, …), defaultFeatures |
| @vexcms/richtext-plate/render | RichText |