Fields
Fields Overview
Vextro uses a type-safe field builder system to define both the Convex schema validator and the admin UI configuration from a single declaration. Every field is created through the f namespace, which provides builder functions for each supported field type.
The field builder syntax
Import f from vextro and use it inside your collection or block definition:
import { f, defineVextroCollection } from "vextro";
export const posts = defineVextroCollection({
slug: "posts",
label: "Posts",
collectionType: "content",
tableName: "posts",
fields: {
title: f.text({ required: true }),
body: f.richText(),
status: f.select({ options: ["draft", "published"] }),
author: f.id("users", { required: true }),
},
}); Each f.<type>() call returns a VextroFieldDefinition that contains a Convex validator and an admin UI config object. When required is false (the default), the validator is wrapped in v.optional() automatically.
Common options
Every field builder accepts these shared options:
| Option | Type | Default | Description |
|---|---|---|---|
required | boolean | false | When true, the Convex validator is required and the admin UI enforces a value. |
label | string | (data) => string | Auto-generated | Custom label for the admin UI. Accepts a static string or a function that receives the document data. |
description | string | — | Help text displayed below the field label. |
readOnly | boolean | false | Renders the field as disabled in the admin UI. |
defaultValue | unknown | — | Default value when creating new documents. The value should match the field's type. |
placeholder | string | — | Placeholder text for text-based inputs. |
condition | FieldCondition | — | Show or hide the field based on sibling field values. Conditional fields are always optional in the schema. |
copyable | boolean | false | Show a copy-to-clipboard button next to the field value. Useful for IDs, slugs, URLs, and API keys. |
tooltip | string | { icon?: "info" | "warning" | "danger", text: string } | — | Tooltip icon next to label. String shorthand uses info icon. |
sidebar | boolean | false | Place the field in the document sidebar instead of the main content area. |
sidebarSection | string | — | Group the field under a named sidebar section. Built-in sections: "document", "actions". Fields without a sidebarSection go to "_default" and render without a collapsible wrapper. |
sidebarCollapsed | boolean | false | Start the sidebar section collapsed for this field. |
listColumn | boolean | false | Include the field as a default column in collection list views. |
listColumnWidth | "auto" | "small" | "medium" | "large" | "auto" | Column width preset for list views. |
searchable | boolean | false | Index the field value for full-text search. |
viewTransition | boolean | false | Animate this field with CSS View Transitions when navigating between list and edit views. |
readRoles | string[] | — | Roles that can see this field. When unset, all roles can read. |
writeRoles | string[] | — | Roles that can edit this field. When unset, all roles can write. Users without write access see the field as read-only. |
hidden | boolean | false | Completely hide the field from users who lack read access. When false, restricted fields are visible but values are omitted. |
unique | boolean | false | Enforce uniqueness for this field. Adds a by_fieldName Convex index and rejects create/update when another document has the same value. f.slug() defaults to true. |
indexed | boolean | false | Identical to unique. Adds a Convex index and enforces uniqueness. |
width | number | — | Width as a percentage (1–100) when this field is inside an f.row(). Fields without an explicit width share the remaining space equally. See Row layout. |
hooks | FieldHooks | — | Field-level hooks for value transformation. Supports beforeChange (transform before write), afterRead (transform after read), afterChange (side effects after write), and beforeDuplicate (transform copied values during duplication). See Hooks. |
How fields map to Convex validators
Each builder produces a specific Convex validator. When required is false, the validator is wrapped in v.optional().
| Builder | Convex Validator |
|---|---|
f.text() | v.string() |
f.textarea() | v.string() |
f.number() | v.number() |
f.email() | v.string() |
f.url() | v.string() |
f.slug() | v.string() |
f.select() | v.union(v.literal(...)) per option |
f.radio() | v.union(v.literal(...)) per option |
f.checkbox() | v.boolean() |
f.date() | v.string() (ISO date) |
f.datetime() | v.number() (Unix ms) |
f.id("table") | v.id("table") |
f.json() | v.any() |
f.group() | v.object({...}) from nested fields |
f.array() | v.array(v.object({...})) from nested fields |
Conditional display
All fields accept a condition option that controls visibility in the admin UI. Conditional fields are always stored as optional in the Convex schema, even when required: true is set. The required validation only applies when the field is visible.
linkType: f.select({
options: ["internal", "external"],
required: true,
}),
externalUrl: f.url({
required: true,
condition: { field: "linkType", equals: "external" },
}), Conditions support equals, notEquals, in, notIn, exists, comparison operators, and logical combinators (and, or, not).