Block Schema & Field Types

All field types, the defineBlock + fields builder, field options, repeaters, and select options.

September 9, 2026

All Field Types

Define a block's editable props with the fields.*() builder inside defineBlock. Each builder returns a field definition; the key you give it in props becomes the content key.

BuilderDescriptionValue Type
fields.text()Single-line text inputstring
fields.textarea()Multi-line text areastring
fields.richText()WYSIWYG editor (Tiptap)string (HTML)
fields.markdown()Markdown editorstring (Markdown)
fields.number()Number inputnumber
fields.date()Date pickerstring (ISO date)
fields.datetime()Date & time pickerstring (ISO)
fields.boolean()Toggle switchboolean
fields.color()Color pickerstring (hex)
fields.media()Image/video uploadResolvedMedia or ResolvedMedia[]
fields.link()Internal/external linkstring
fields.url()URL inputstring
fields.email()Email inputstring
fields.select()Dropdown (single choice)string
fields.radio()Radio group (single choice)string
fields.multiselect()Multiple choicestring[]
fields.relation()Reference records from a modelrecord or record[]
fields.repeater()Array of nested field groupsobject[]
fields.table()Editable table grid{ columns, rows }
fields.json()Raw JSON editorJSON value
fields.form()Pick a Cmssy formstring (form id)
fields.pageSelector()Pick page(s)PageRef[]

Most builders take only the base options below. fields.select(), fields.radio() and fields.multiselect() require an options array; fields.media() accepts multiple; fields.relation() needs a model (the referenced model's slug) and optionally mode: "all", multiple, sort and limit. A relation stores record id(s); the delivery API resolves them to full records before your component renders.


Defining a Block

A block is declared with defineBlock. Its editable fields live under props:

import { defineBlock, fields } from "@cmssy/react";
import Hero from "./Hero";

export const heroProps = {
  heading: fields.text({ label: "Heading", required: true, defaultValue: "Welcome" }),
  description: fields.textarea({ label: "Description" }),
};

export const heroBlock = defineBlock({
  type: "hero",
  label: "Hero",
  component: Hero,
  props: heroProps,
});

Export the props object on its own and type the component from it - BlockProps<typeof heroProps> - so the schema stays the only place a field is named, and the value types in the table above become the types your component sees. See Block development.


Field Options

Every fields.*() builder accepts the same base options:

fields.text({
  label: "Heading",
  required: true,
  defaultValue: "Welcome",
  placeholder: "Enter text",
  helperText: "Main heading",
})

Available keys: label, required, defaultValue, placeholder, helperText, localized. Select and repeater fields add a few more (below).


Fields That Are the Same in Every Language

By default every field is translated: each language holds its own value. A field that should hold one value for the whole site - a logo, a product photo, a brand colour - declares localized: false:

props: {
  logo: fields.media({ label: "Logo", localized: false }),
  heading: fields.text({ label: "Heading" }),
}

The editor shows such a field once, with a padlock, whichever language you are editing; changing it changes it for every language. The delivery API still returns it inside every language's content, so your component reads content.logo exactly as before - nothing changes on the consumer side.

localized is independent of tab: a field on the style or advanced tab is already one value per block, so the flag adds nothing there. Flipping the flag on an existing field moves the stored value for you on the next manifest push. Requires @cmssy/core 16.4.0 or newer.


Select & Multiselect

Options are a plain array of strings:

props: {
  layout: fields.select({
    label: "Layout",
    options: ["grid", "list", "carousel"],
    defaultValue: "grid",
  }),
  tags: fields.multiselect({
    label: "Tags",
    options: ["featured", "new"],
  }),
}

Repeater (Arrays)

Use itemSchema — a map of nested fields.*() — to describe each row:

props: {
  features: fields.repeater({
    label: "Features",
    maxItems: 6,
    itemSchema: {
      icon: fields.text({ label: "Icon Name", defaultValue: "Star" }),
      title: fields.text({ label: "Title", required: true }),
      description: fields.textarea({ label: "Description" }),
    },
  }),
}

In your component:

export default function Features({ content }) {
  return (
    <div className="grid grid-cols-3 gap-6">
      {(content.features ?? []).map((f, i) => (
        <div key={i}><h3>{f.title}</h3><p>{f.description}</p></div>
      ))}
    </div>
  );
}

Repeater also supports itemLabel, addButtonLabel, minItems, maxItems, and collapsible.