Fields
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#
All fields accept these:
| 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 the translations table |
indexed |
boolean |
Add a database index |
unique |
boolean |
Enforce unique values (defaults to true for slug, false otherwise) |
condition |
{ field, value } |
Show/hide based on another field |
admin.placeholder |
string |
Input placeholder text |
admin.rows |
number |
Textarea height (text fields) |
admin.help |
string |
Help text below the input |
admin.position |
"content" | "sidebar" |
Where the field renders in the edit form. Defaults to "content" |
admin.group |
string | object |
Titled, optionally collapsible panel — consecutive fields sharing a group render together |
admin.hidden |
boolean |
Hide from the admin UI |
admin.component |
string |
Custom admin component |
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 |
content |
blocks |
object |
Inline component block 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 |
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 are 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 with 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.
Content (prose + inline blocks)#
fields.content(...) is for long-form editing where prose and inline component blocks 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: [] }),
},
},
});Each inline block uses a blockType and fields payload, similar in shape to fields.blocks(...), but the block appears inside the writing flow rather than 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 fields.blocks(...) when the editor should manage standalone, reusable page sections.
When fullscreen is enabled, a maximize button appears in the editor's top-right corner. It opens a distraction-free overlay that hides everything else and centers the writing column; Escape or Exit returns to the normal form with edits preserved.
Item limits and ordering#
List-shaped fields (relation with hasMany, array, and json repeaters) accept maxItems — enforced centrally on save and reflected in the admin picker with a 2/4 selected counter:
newsItems: fields.relation({
collection: "articles",
hasMany: true,
maxItems: 4,
}),The order of a hasMany selection is data — stored and rendered as-is. The admin renders selections as drag-sortable rows (grip handle, #index, remove) by default, no option needed.
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 palette
backgroundColor: fields.color(),Override the palette for a single field with colors — it wins over the global list.
Shared sections#
Editors can save a fields.blocks(...) block as a shared section, insert it into other block fields, and detach it later into a local copy. Shared sections live in the admin sidebar under Shared Sections, stored as normal draft/versioned content — the right fit for reusable heroes, CTAs, FAQ sections, or feature grids.
Set shared: false for block fields that should never use shared sections, such as form builders. Shared sections can be inserted from the editor's / slash menu; a regular block can be saved as a shared section, and a shared block can be detached back into an editable copy.
Conditional fields#
Show or 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" },
}),value can be a string, boolean, or array of strings (matches any).
Field-level access#
Fields support read and update access rules:
import { hasRole } from "@kidecms/core";
summary: fields.text({
access: {
read: hasRole("admin"), // hidden from non-admins
},
}),
seoDescription: fields.text({
access: {
update: hasRole("admin"), // read-only for non-admins
},
}),| Rule | Effect in admin UI | Effect on save |
|---|---|---|
read |
Field is completely hidden | Field excluded from response |
update |
Field is rendered read-only (disabled) | Field value silently preserved (changes stripped) |
Both rules receive the same context as collection-level access rules — see Access Control.
AI assistant#
The admin includes optional AI features powered by the Vercel AI SDK. Add AI_PROVIDER=openai and AI_API_KEY to .env to enable them; AI_MODEL is optional and defaults to gpt-4o-mini. Currently openai is the only supported provider.
When configured, AI buttons appear automatically for:
- Alt text generation on asset detail pages
- SEO descriptions on post/page edit forms
- Translation, with per-field "Translate from EN" buttons that handle both plain text and rich text (preserving JSON AST structure)
Without the AI env vars, all AI buttons are hidden and no AI dependencies are loaded.