Skip to content

Fields

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

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)
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".

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.

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.

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.

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 palette
backgroundColor: 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>}

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.

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.

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.