Accent

Fields

The reusable field registry, data type vs editor, field sets and namespaces, options vs content.


Every field

Text

The single-line string family — one stored contract, three variants that differ only in what the boundary promises.

Text · Email · URL

Long text

The paragraphs family — one text data type, two variants differing in what the value promises.

Textarea · Rich text

Number

The arithmetic family — real numbers, and the digits that are not math.

Float · Integer

Toggle

One yes-or-no fact, stored true/false — three affordances, and the three-state smell that outgrows them.

Switch · Checkbox · Radio

Options

Declared legal values — four methods, two stored shapes, cardinality is the contract.

Select · Radio · Checkboxes · Multi-select

Datetime

One temporal data type, three granularities — the method fixes which form a field stores.

Date & Time · Date · Time

Images

The pictures kind — what a template owes an ; two shapes, one promise.

Single · Multiple

Files

The non-image kind — documents and uploaded video; linked or embedded, never rendered.

Single · Multiple

Media collection

An ordered list of media, where the order is content.

Reference

Point at another asset — identity, not a copy; one authoritative source, followed at render.

Single · Multiple

Group

Fields that travel together — declared once, nested, judged where they live.

Repeater

Rows of the same shape — order is content, bounds are rules, conditions gate per row.

Fields are the data schema of everything authored. They are site-level, reusable definitions: a field is created once, in the site's registry, and assembled into content types — the same start_date can serve an Event, a Class, and a Special without being declared three times. A content type (see chapter 1) is, at its center, a list of fields.

The field registry

Every field carries three names, doing three jobs:

Name What it is
Label The display name editors see. Leads the create form — type "Start Date" and the rest follows.
Key The public data identifier (start_date) — what templates and the API read. Minted from the label; letters, numbers, hyphens, underscores.
id The stable internal identity (fld_…). Storage keys on it, so a field can be relabeled — or rekeyed — without orphaning stored values.

The registry lives at Manage Site → Fields (the surface is covered in the admin guide), organized by the same folder primitive as templates and types.

Data type and editor

A field declares two separable things: what is stored and how it is edited. The data type is a contract; the editor is a choice — and most of what feels flexible about the field system comes from refusing to fuse them.

  • The data type is the stored value's shape: string, text, number, boolean, date, json, media, reference, collection.
  • The editor is the editing experience: text_input, textarea, rich_text, date_picker, date_time_picker, toggle, media_picker, image_picker, file_picker, a select with declared options, a reference picker, raw JSON.

The split, and where it comes from

This is ProcessWire's insight, and the credit belongs there. ProcessWire separates the Fieldtype (what a field stores, how it queries) from the Inputfield (the form control that collects it), and lets a Fieldtype offer a choice of Inputfields — their Textarea can be edited as a plain textarea or a rich-text editor, and both are modules a third party can add to. Twenty years of that architecture holding up is most of the argument for it; ours is the same idea with the names data type and editor.

The alternative is the common one: a flat list where each field type IS its editing widget. It reads simpler until the first time one stored shape needs a second control — then either the widget grows modes, or a parallel field type appears whose values are the same but whose name is different, and templates start caring which control the author picked. Templates should never know that.

One contract, many editors

The same stored shape, edited three ways, chosen by the shape of the set and never changing what a template reads:

Stored (data type) Editors Choose by
text textarea · rich_text whether authors may format
string + options select one from a closed set
json (list of option values) checkbox_group · multi_select set size — boxes to about seven, the dropdown past that
media image_picker · file_picker · media_picker what the field accepts
date date_picker · date_time_picker whether the hour matters

Switching the multi-choice field from checkboxes to the dropdown is the same move as switching a bio from textarea to rich text: an editing decision, made per field, with the stored value untouched. The options page walks both cardinalities of the worked example.

The compatibility contract

Editors are modules. Each one registers which editors it renders and which data types it can serve — that pairing is the compatibility contract, and the registry derives the choices the admin offers from it. The core set covers the table above; a module can add an editor (or deliberately replace one) without touching the data types, which is also ProcessWire's arrangement: Fieldtypes and Inputfields ship as plugins, and most third-party ones arrive in pairs.

Two consequences worth naming:

  • Templates read the contract, not the choice. content.audiences is a list of option values whichever control collected it. Nothing downstream — templates, the API, queries — can tell the editors apart, which is the test the split has to pass.
  • The boundary judges the contract, not the choice. Validation runs against the data type and the field's declared rules (see fields); an editor is trusted to produce well-shaped values but never relied on for it, because the same field is writable through the API and an agent, where no editor exists.

The neighbours, linked

Two systems worth reading beside this chapter, one for each side of the argument:

  • ProcessWire — Fields, Fieldtypes and Inputfields — the origin of the storage/input split, running in production for two decades. Their Textarea choosing between a plain textarea and a rich-text editor is this chapter's texttextarea | rich_text, twenty years older.
  • Carbon Fields — the honest counter-example: a flat list where each field type is its editing widget. Simpler to learn, and the cost shows at the seams — an image field that stores an id or a URL depending on a per-field flag. Its complex field is also the named-groups precedent this system's field conditions were measured against.

Where the seam sits in practice

A field's editor can change after content exists. Storage keys on the field's identity, values are validated against the data type, so flipping audiences from checkbox_group to multi_select re-renders the form and re-reads the same stored list. The reverse — changing the data type — is a migration, not a preference, which is exactly the asymmetry the two names are there to keep visible.

Field sets and namespaces

A content type declares one field list, and every entry carries a namespace. The sets emerge from the tagging — "the content fields" are simply the entries tagged content:

Namespace What it holds
content The asset's own data — what the thing says.
meta Title/SEO/social fields, attachable as schemas.
options Render configuration — what templates and layouts read.

Values follow the same shape, nested under one container: data.content, data.meta, data.options. A type with no options declares no options set — absence costs nothing, in storage, in projections, and in the editing UI.

Options are not content

Options deserve their own sentence because they are structurally fenced: they are render config (width, alignment, variant…), read by templates and layouts, and excluded from every SEO, sitemap, and social surface. They also have their own transport: when a fragment output is mounted with per-mount options (see rendering), the call-site values merge over the asset's stored options — never into its content. Content is what the asset says; options are how this rendering of it behaves.

Reading fields in templates

Templates read keys, never storage ids:

<time datetime="<%= cms.asset.content.start_date %>">
  <%= cms.asset.content.start_date %>
</time>

The same address works on every projection of an asset — the current page (cms.asset.content.*), a query row (row.content.* from cms.assets.find()), and a fragment render all decode the stored bag through the type's field definitions, so one partial can read any of them. The namespaced bag is also addressable directly: cms.asset.data.content.*, data.meta.*, data.options.*.

Sanitize and validate at the write boundary

A field can declare two rule bags, and they are two different mechanisms:

  • sanitize transforms. It canonicalizes the value — trim, collapse_whitespace, lowercase/uppercase, strip_html — and what it produces is what gets stored. Rules apply in a fixed canonical order and every rule is idempotent, so a re-save changes nothing.
  • validate judges. It checks the sanitized value — required, min/max, max_length, pattern (with an optional message), email (the WHATWG definition — what the author's browser would say), url — and reports issues without changing anything.

Sanitize runs first, so validation and storage always see the same value. Both bags are plain data on the field definition, enforced per data type at the write boundary, whichever lane the write arrives through (the admin, the web API, or an agent). Issues come back with the write response — path, code, and a human message per problem.