Fields
Field types
Section titled “Field types”| Field | Storage | Admin Component |
|---|---|---|
text |
text |
Input or Textarea (with rows) |
slug |
text (unique) |
Auto-generated from source field |
email |
text |
Email input |
number |
integer |
Number input |
boolean |
integer (0/1) |
Checkbox |
date |
text (ISO 8601) |
Date picker |
select |
text |
Select dropdown |
color |
text (hex) |
Palette swatch dropdown |
link |
text (JSON) |
Internal/external link control |
richText |
text (JSON AST) |
Tiptap editor |
content |
text (JSON AST) |
Tiptap editor with inline blocks |
image |
text |
Image picker with upload/browse |
relation |
text (reference ID) |
Combobox with search |
array |
text (JSON) |
Comma-separated input (of: fields.text()), else JSON textarea |
json |
text (JSON) |
Textarea or custom component |
blocks |
text (JSON) |
Drag-and-drop block editor |
Common options
Section titled “Common options”All fields accept these options:
| Option | Type | Description |
|---|---|---|
required |
boolean |
Validate as non-empty on save |
label |
string |
Custom label (defaults to humanized field name) |
description |
string |
Text shown below the label |
defaultValue |
varies | Initial value for new documents |
translatable |
boolean |
Store per-locale in translations table |
unique |
boolean |
Enforce unique values (defaults to true for slug, false otherwise) |
condition |
{ field, value } |
Show/hide based on another field — see Conditional fields |
access |
{ read?, update? } |
Per-field access rules — see Field-level access |
admin |
object |
Admin-only presentation options, listed below |
admin sub-options:
| Option | Type | Description |
|---|---|---|
admin.component |
string |
Built-in variant or custom component (see Admin UI) |
admin.placeholder |
string |
Input placeholder text |
admin.position |
"content" | "sidebar" |
Where the field renders in the edit form. Defaults to "content" |
admin.rows |
number |
Textarea height — turns a text input into a textarea; also sizes json textareas and the richText / content editors |
admin.help |
string |
Help text below the input |
admin.hidden |
boolean |
Hide from the edit form (the field still exists in the API) |
admin.group |
string | { label, collapsible? } |
Titled, optionally collapsible panel in the edit form — consecutive fields sharing a group render together (see Admin UI) |
admin.colors |
{ label, value }[] |
Per-field palette for fields.color(); overrides the global admin.colors (see Colors) |
Type-specific options
Section titled “Type-specific options”| Field | Option | Type | Description |
|---|---|---|---|
text |
maxLength |
number |
Maximum character length, validated on save |
slug |
from |
string |
Field name the slug is auto-generated from |
select |
options |
string[] (required) |
Allowed values |
relation |
collection |
string (required) |
Target collection slug |
relation |
hasMany |
boolean |
Store an array of IDs instead of one |
relation |
maxItems |
number |
Max selected documents (hasMany only) |
array |
of |
field (required) | Item field type, e.g. fields.text() |
array |
maxItems |
number |
Max items, enforced on save |
json |
itemFields |
Record<string, field> |
Typed repeater rows (with admin.component: "repeater") |
json |
maxItems |
number |
Max rows when the value is an array (repeaters), enforced on save |
color |
colors |
{ label, value }[] |
Per-field palette; shorthand for admin.colors |
content |
blocks |
object |
Inline component block types (same shape as blocks types) |
content |
fullscreen |
boolean |
Distraction-free overlay button. Defaults to true |
blocks |
types |
object (required) |
Block types, each a map of sub-fields |
blocks |
shared |
boolean |
Allow shared section references. Defaults to true |
email, number, boolean, date, richText, image, and link take only the common options. color and link are shorthands: fields.color() is a text field with admin.component: "color", and fields.link() is a json field with admin.component: "link".
Content
Section titled “Content”Use fields.content(...) for long-form editing where prose and inline component blocks should live in one ordered stream, such as a blog post body.
body: fields.content({ translatable: true, admin: { rows: 14 }, fullscreen: true, blocks: { faq: { heading: fields.text(), items: fields.json({ admin: { component: "repeater" }, }), }, image: { images: fields.array({ of: fields.image(), defaultValue: [] }), }, },});The field stores a rich-text document whose children can also include inline component blocks. Each inline block uses a blockType and fields payload, so the schema is similar to fields.blocks(...), but the block appears inside the writing flow instead of as a separate page-section list.
Render content fields with ContentRenderer, which interleaves prose and the configured block components. Use fields.richText(...) when the editor only needs formatted text, and use fields.blocks(...) when the editor should manage standalone reusable page sections.
fullscreen (default true) adds a button that opens the editor in a distraction-free overlay. Escape exits; edits stay in the form until you save.
Blocks
Section titled “Blocks”fields.blocks({ types: { hero: { heading: fields.text({ required: true }), body: fields.text(), ctaLabel: fields.text(), ctaHref: fields.text(), }, text: { heading: fields.text(), content: fields.richText(), }, faq: { heading: fields.text(), items: fields.json({ defaultValue: [], admin: { component: "repeater" }, }), }, },});Block sub-fields accept any field type. Common choices: text, number, boolean, select, richText, image, relation, array, json, color.
The repeater component renders JSON arrays as sortable add/remove item cards. Declare
itemFields to give rows typed controls instead of free-form JSON — including link
sub-fields, which get the internal-link picker:
services: fields.json({ admin: { component: "repeater" }, itemFields: { title: fields.text({ required: true }), body: fields.text({ admin: { rows: 3 } }), link: fields.link(), },}),Repeaters work both as block sub-fields and as top-level collection fields — useful for fixed-slot templates where a section holds a list of cards.
Item limits and ordering
Section titled “Item limits and ordering”List-shaped fields (relation with hasMany, array, and json repeaters) accept
maxItems — a declarative cap enforced centrally on save (Field "x" allows at most N items.)
and reflected in the admin picker, which shows a 2/4 selected counter and stops accepting
picks at the limit:
newsItems: fields.relation({ collection: "articles", hasMany: true, maxItems: 4 }),The order of a hasMany selection is data: it is stored and rendered as-is. The admin
renders selections as drag-sortable rows (grip handle, #index, remove) by default.
No option needed.
Colors
Section titled “Colors”fields.color(...) stores a hex string and renders a dropdown of predefined swatches. Editors pick a named color instead of typing a hex value.
Define the palette once in your CMS config and every color field offers it:
export default defineConfig({ admin: { colors: [ { label: "Blue", value: "#4000FF" }, { label: "Pink", value: "#FFDBEB" }, { label: "Black", value: "#000000" }, ], }, collections: [...],});// Uses the global admin.colors palettebackgroundColor: fields.color(),Override the palette for a single field with colors — it wins over the global list:
accentColor: fields.color({ colors: [ { label: "Brand", value: "#4000FF" }, { label: "Ink", value: "#000000" }, ],}),The stored value is the selected hex string ("" when cleared). Editors can’t enter a free-form hex value. Color fields work both as top-level fields and as block sub-fields.
fields.link() renders a structured link control: an internal document picker or an external URL, with a label and a new-tab toggle. It works as a top-level field, a block sub-field, and a repeater row field.
cta: fields.link({ label: "Call to action" }),The stored value is a JSON object:
{ type: "internal" | "external", url: string, // cached href label?: string, // link text typed by the editor; may be empty title?: string, // internal links: the picked document's title newTab?: boolean, docId?: string, // internal links: the picked document collection?: string, // internal links: its collection slug}Internal links keep a document reference alongside the cached url, so they survive slug edits. Resolve the current href at render time with resolveLinkUrl; it falls back to the stored url for external links, hand-typed paths, and unpublished or deleted targets:
---import { cms } from "@/cms/.generated/api";import { resolveLinkUrl } from "@kidecms/core";
const href = await resolveLinkUrl(cms, doc.cta);---
{href && <a href={href} target={doc.cta.newTab ? "_blank" : undefined}>{doc.cta.label || doc.cta.title}</a>}Shared sections
Section titled “Shared sections”Editors can save a block as a shared section, insert it elsewhere, and detach it later to make a local copy. Shared sections live in the admin sidebar under Shared Sections and are stored as normal draft/versioned content. Editing the source updates every place that uses it.
They are on by default in fields.blocks(...). Set shared: false for block fields that should never use them, such as form builders:
fields.blocks({ shared: false, types: { text: { name: fields.text({ required: true }), label: fields.text({ required: true }), }, },});In fields.content(...), insert a shared section from the / slash menu. Inline blocks have the same save-as-shared and detach actions.
Conditional fields
Section titled “Conditional fields”Show/hide fields based on a select or boolean field’s value:
postType: fields.select({ options: ["article", "video", "podcast"],}),videoUrl: fields.text({ condition: { field: "postType", value: "video" },}),The value can be a string, boolean, or array of strings (matches any). Conditions also work on block sub-fields — inside fields.blocks() types, inline content blocks, shared sections, and repeater rows — where field refers to a sibling sub-field in the same block or row.
Field-level access
Section titled “Field-level access”access: { read, update } on a field hides it or makes it read-only for users the rule denies, enforced in the API as well as the admin. See Field-level access.