Rows of the same shape — order is content, bounds are rules, conditions gate per row.
Rows of the same shape, as many as the content needs: FAQ entries, the media items on an exhibition, a band's tour dates. A repeater is a group that repeats — each row carries the same declared children — and it is the answer this model gives where others reach for a page builder.
The name is ProcessWire's, and the heterogeneous form — rows whose shape depends on a per-row choice — is the pattern Carbon Fields calls a complex field: here it is a repeater whose row holds an options field plus condition-gated groups, no special machinery.
An ordered list of row objects, each keyed by the row's children:
{ "media_items": [
{ "media_kind": "photograph", "caption": "Jellyfish, backlit" },
{ "media_kind": "movie", "video": "…" }
] }
Order is content — rows reorder by the row controls, and the
template renders in storage order. sortable is on by default because
ordering is usually the point; a repeater declares min/max when
the count has real bounds.
| Label | Media items |
| Key | media_items |
| Kind | repeater |
| Row | the child assignments — inline, or a registry group reused by reference |
| Bounds | min: 1, max: 12 — row-count validation |
Row counts are judged first, then every field of every row, at its
row's path: a 2.5 in row three's seat count reports at
data.media_items[2].seats, an off-list value in a row's select is
refused by the same one_of that guards it at top level. Per-row
conditions evaluate against that row's own values —
the photograph row's caption is judged, the movie row's hidden caption
keeps its value and stays quiet.
<% for (const item of media_items ?? []) { %>
<% if (item.media_kind === 'photograph') { %>
<figure><img src="<%= item.photograph?.image?.src %>">
<figcaption><%= item.caption %></figcaption></figure>
<% } %>
<% } %>