Section field types
Reference of the field types available in a section's schema, the editor control each one renders, and how to configure it.
Each editable field in a section's schema has a type that selects its editor
control in the properties sidebar. This page lists every available type and how
to configure it. See
Create custom sections for how a schema
fits into a section.
Every key in a section's schema mirrors a key in its defaultProps; your
component then receives the edited values as props.
Two attributes apply to every type: attributes.comment is shown to the
contributor as a hint under the control (and announced with it), and
attributes.required marks the field as required.
Conditional fields
Any field can carry a condition that gates its visibility on a sibling field
of the same schema. The field renders only when the sibling's effective value —
its current value, or its default_value when unset — matches equals,
strictly, or by membership when equals is an array. Omit condition for the
fields that are always visible.
mode: {
type: "select",
label: "Categories to show",
default_value: "all",
options: [
{ label: "Every category", value: "all" },
{ label: "Picked categories", value: "selected" },
],
},
level: {
type: "select",
label: "Category level",
default_value: "all",
condition: { field: "mode", equals: "all" },
options: [
{ label: "Every level", value: "all" },
{ label: "Level 1", value: "1" },
],
},
Hiding a field clears nothing: the value a content manager entered stays in the
section's props, and the published page hands all of them to your resolver. So a
field shown by a condition the section no longer displays still needs a defined
meaning — the built-in CategoryList reads its mode first and ignores the
props of the branch the mode did not choose, which is what lets a contributor
switch back and forth without losing their pick. In the editor's preview the
resolver only sees the props that travel, which a section has to declare: see
create custom sections.
checkbox
A boolean on/off toggle.
{ type: "checkbox", label: "Show divider", default_value: false }
color
Round swatches from the themed palette. The selected colour is named next to the label, and a value outside the palette is kept as a custom swatch.
{ type: "color", label: "Text color", default_value: "#111827" }
A field whose default_value is "" (or with
attributes: { allowEmpty: true }) also offers a Default swatch that stores
"", so the contributor can return to the theme colour after picking one.
Set attributes: { transparent: true } to add a Transparent swatch
(persisted value transparent). It is also the only way to clear a selected
colour back to none — a radio group cannot be unset.
{
type: "color",
label: "Background",
default_value: "",
attributes: { transparent: true },
}
datasource
A picker backed by a data source — a typed catalog entity (a category, a product…) resolved server-side by the active backend. The contributor searches the source in a combobox (the term is sent to the backend, results are capped) and picks an entity; the field then shows it as a card with its key and Replace / Remove actions, and stores the entity's key (its id, or a product SKU). A key can also be typed by hand behind the "Enter a key manually" fold, which opens by itself when the source lists nothing. The entity itself is delivered to your component as dynamic data through a matching GraphQL field (see Dynamic sections).
{ type: "datasource", dataSource: "product", label: "Product", default_value: null }
dataSource names the source (for example category or product). The
available sources depend on the backend; a source the backend does not implement
yields an empty picker.
Contextual options
By default, a source lists its options from the whole catalogue. A field can
declare optionsContext — an array of section keys — to make its options
contextual: the raw current values of those keys travel to the source along
with the section's type, and the source computes its options on that state
instead. The built-in case is the Product List on the Gezy flavor, whose facet
pickers list what the current search still allows, like the search page's
filters do: a brand already picked stays proposable (a search accepts several),
a value the search excludes never shows.
{
type: "datasource",
dataSource: "brand",
label: "Brands",
default_value: null,
optionsContext: ["searchQuery", "onlyPromotions", "brands"],
}
Only the keys the field declares travel — none of the section's other values
does — and they go as authored: the editor collects them without knowing
their semantics, and the source reads them server-side. The built-in Gezy
pickers read them as the section's current search; a source you register
yourself receives the same context as listOptions's third parameter and can
narrow its options however it sees fit. In a repeater, every row's picker shares
the section's context — a row never recomputes it from its own values.
Options are computed with the values current when the properties sidebar
opens: the picker captures its context once, so editing the term or the facets
afterwards does not recompute the list — closing and reopening the sidebar does.
A field without optionsContext, or a source that ignores the context, behaves
as before: the whole catalogue.
Sources per flavor
| Source | Gezy | Magento 2 | Magento 1 |
|---|---|---|---|
category | ✅ | ✅ | ✅ |
product | ✅ | ✅ (search: the configured engine, or Magento's native search fallback) | ✅ (list: requires an active search engine) |
user | ✅ | ✅ | ✅ |
brand | ✅ | ✅ | — |
searchCategory | ✅ | ✅ | — |
caracValue | ✅ | ✅ | — |
A source a flavor does not implement degrades quietly: the field resolves to
null and the picker lists no option, without an error.
The last three sources are picker-only: they resolve no entity — resolve
answers null — because their picks are not data to display but filters the
section's own search reads (the Product List's "Search" source). brand and
searchCategory list the id buckets of the search's brand and category facets,
caracValue one option per characteristic bucket, labelled "Characteristic —
Value" and storing the [code, value] pair the search expects. When the field
declares optionsContext, their buckets are
recomputed on the section's current search: an already-set brand or category
stays pickable (the search accepts several, in an OR), while an already-set
characteristic keeps its filter but its buckets leave the picker (one value per
characteristic). searchCategory serves the search's filter categories —
the same ids as the category source's navigation tree, so picks made through
either source stay valid. All three cap their list (20 options) and narrow it by
the picker's own search term.
On Magento, the category picker lists the first three levels of the
navigation tree, indented; a deeper category is not selectable, and the search
term filters the loaded levels rather than querying the catalog again. The
Category List section stops at the same three levels: its level field offers
1 to 3 on every backend, whatever the navigation actually serves, and a
deeper level is only reachable through "Every level" — on a backend serving more
than three levels, the section cannot list the fourth alone. The product
picker is the other way round: it lists nothing until the contributor types a
term, which is sent to the search backend.
The picker also accepts a key typed by hand (a SKU for product, an id for
category), next to the list. Contributors who know their catalogue go straight
to the key, and it is the way to contribute when the source returns no option at
all. The key is applied with Enter, with the Apply button, or by leaving the
field — never while typing, so a partial key is never stored. It is not verified
against the backend: the section preview is the feedback, and a key that
resolves to nothing renders the section's empty state.
On Magento 1 the product picker lists options only when a search engine (ElasticSearch, Algolia, Attraqt) is active: there is no native full-text product search to fall back on, so without an engine the list stays empty and contributing goes through the manual key. A product already picked still renders. Every other source, and the Category Products Grid section, work without an engine.
The picker is empty as well while the engine is down, on both Magento
flavors: the search layer answers an error fallback instead of failing. When
that happens the server logs a warning —
The search engine answered its error fallback — so an empty picker can be told
apart from a genuine "no result".
If the manual key is not enough for your project, register your own
product source: the registry is a map where the last registration wins.
services.DI.get("cms").dataSources.register("product", myProductDataSource);
Register it from afterServerServicesInit, or from onServerServicesInit
of an extension declared after cms() in the extensions array of
front-commerce.config.ts. Hooks run in declaration order, so an extension
declared before cms() would see its source overwritten by the CMS one.
date
A native date picker. The value is an ISO YYYY-MM-DD string.
{ type: "date", label: "Publication date", default_value: "" }
image
An image picker storing a source URL and alt text. It opens the built-in media library, where the content manager browses folders and picks or uploads an asset. The inspector shows the media tile under the field label, then the alternative text indented behind a rail: a compound field displays its value first and the settings that qualify it below.
{ type: "image", label: "Illustration", default_value: { src: "", alt: "" } }
link
The same input as url, for internal or external links.
{ type: "link", label: "Button link", default_value: "/contact", attributes: { placeholder: "https://…" } }
margin
A spacing editor for the four margins (unit + lock to edit all sides at once).
{
type: "margin",
label: "Margin",
default_value: { top: 0, right: 0, bottom: 0, left: 0, unit: "px", locked: true },
}
media
A media picker storing the selected file's URL as a plain string (no alt text).
It opens the same media library as image.
{ type: "media", label: "Attachment", default_value: "", attributes: { placeholder: "https://…" } }
number
A number. With both min and max it renders as a slider; without bounds it
renders as a numeric input, since a slider needs both ends to mean anything.
attributes.unit is shown after the value ("80 %").
{ type: "number", label: "Columns", default_value: 3, attributes: { min: 1, max: 6, step: 1 } }
The min, max and step attributes also accept a field reference
{ field: "<key>" } pointing at another field of the section's root schema. At
render time the editor replaces the reference with the referenced field's
effective value (its current value, or its default_value when unset) — passed
through as is, without clamping: the referenced field's own bounds are the
envelope. This lets one field's bounds follow another field's live value, e.g. a
tile's column span bounded by the section's column count:
{
columns: { type: "number", label: "Columns", default_value: 4, attributes: { min: 1, max: 8, step: 1 } },
tiles: {
type: "repeater",
label: "Tiles",
itemLabel: "Tile",
default_value: [],
itemSchema: {
colSpan: {
type: "number",
label: "Column span",
default_value: 1,
// Bounded by the section's live `columns` value — even from an
// item field, the reference always targets the root schema.
attributes: { min: 1, max: { field: "columns" }, step: 1 },
},
},
},
}
A reference that cannot be resolved (unknown key, non-numeric value) omits the bound and logs a warning in development; the slider then falls back to its own default bound.
padding
The same spacing editor as margin, for paddings.
{
type: "padding",
label: "Padding",
default_value: { top: 16, right: 16, bottom: 16, left: 16, unit: "px", locked: true },
}
range
Always a slider, for a bounded range. attributes.unit is shown after the
value. Its min, max and step attributes accept the same field
references { field: "<key>" } as number.
{ type: "range", label: "Opacity", default_value: 100, attributes: { min: 0, max: 100, step: 1, unit: "%" } }
repeater
A repeatable list of sub-items — use it for FAQs, testimonials, link lists,
tabs, and any variable-length collection. Each item is an object edited through
its own nested form, described by itemSchema (a schema made of the field
types on this page). Your section receives the value as an array of those
item objects.
{
type: "repeater",
label: "Links",
itemLabel: "Link",
default_value: [{ label: "", url: "", description: "" }],
itemSchema: {
label: { type: "text", label: "Label", default_value: "" },
url: { type: "link", label: "URL", default_value: "" },
description: { type: "text", label: "Description", default_value: "" },
},
}
itemLabel (optional) names each entry in the editor (for example "Link 1").
Content managers can add, remove, and reorder items.
An itemSchema may also hold a datasource field, so each row
picks one catalog entity — that is how a "list of products" section lets the
content manager choose N products in their own order:
{
type: "repeater",
label: "Products",
itemLabel: "Product",
default_value: [],
itemSchema: {
sku: { type: "datasource", dataSource: "product", label: "Product", default_value: null },
},
}
The editor preview resolves those keys like a top-level datasource field, so
the section shows the same content in the editor as on the published page. Only
the datasource keys of a row are sent for resolution: the editorial fields
next to them never trigger a new request.
rich_text
A rich-text editor. The value is a TipTap document
(JSONContent), not a plain string.
{ type: "rich_text", label: "Body", default_value: "" }
Alongside text formatting, the toolbar lets content managers add links, insert
images from the media library, and build tables. Rendered headings get a
slugified id, so they can serve as in-page anchor targets.
The </> button opens the HTML source of the text for quick edits or
pasting formatted content. The HTML is a view, not the stored format: when the
source is applied, the editor parses it back into its document and drops the
tags and attributes it does not support (<div>, class, style, <script>,
<iframe>…), then says so and shows the HTML it kept. Escape leaves the source
view without applying.
Render it in your section with the TiptapContent theme component — the same
one the built-in sections use:
import TiptapContent from "theme/modules/CmsPage/sections/tiptapRenderer";
// `body` is the `rich_text` prop your section receives
<TiptapContent content={body} />;
It renders the document with the theme's own atoms (headings, paragraphs, links). Two ways to customise it:
- per usage — pass its
componentsMapprop to override how individual nodes or marks render (entries you omit fall back toDEFAULT_TIPTAP_COMPONENTS); - globally — override it like any theme component by shadowing
theme/modules/CmsPage/sections/tiptapRendererin your own theme, which also changes how the built-in sections render rich text.
select
A single choice among the options you provide.
{
type: "select",
label: "Alignment",
default_value: "center",
options: [
{ label: "Left", value: "left" },
{ label: "Center", value: "center" },
{ label: "Right", value: "right" },
],
}
Up to four options with short labels (12 characters or fewer) render as a
segmented control; larger sets render as a native <select>, which the
inspector's 280 px can hold. Force either control with
attributes: { display: "picker" } or attributes: { display: "dropdown" }.
{
type: "select",
label: "Icon",
default_value: "checkmark",
attributes: { display: "dropdown" },
options: [
/* …10+ options… */
],
}
text
A single-line text input.
{ type: "text", label: "Heading", default_value: "Welcome", attributes: { placeholder: "Enter a heading" } }
textarea
A multi-line text input.
{ type: "textarea", label: "Summary", default_value: "", attributes: { placeholder: "Short summary…", rows: 4 } }
title
A structured heading editor (text, level, size, weight, alignment). The text
input carries the field label; alignment, colour and the typography fold sit
indented behind a rail, as settings of that text. The optional overline
sub-object renders a small editorial label above the title ("pill" badge by
default, or plain "text"; empty means no overline).
{
type: "title",
label: "Title",
default_value: { text: "Section title", level: "h2", size: "text-4xl", weight: "font-bold", alignment: "center", overline: { text: "New arrivals", style: "pill" } },
}
Set attributes: { overline: false } on a title field whose heading the
storefront renders without an overline (e.g. a decorative secondary title): the
overline input is then hidden, so editors cannot author a label that would not
be rendered.
url
A URL input.
{ type: "url", label: "Video URL", default_value: "", attributes: { placeholder: "https://…" } }