Conditional rendering
visibleIf, path syntax, operators, composite conditions, and whenHidden — make fields appear and disappear based on the values of other fields.
The CMS supports field-level conditionality via the visibleIf knob on every
field. A field with visibleIf only renders when its condition evaluates
true; when false it's hidden from the editor AND skipped by validation (so
a hidden required field doesn't block Publish with "is required" the author
can't see).
This is the schema-language feature that lets you build forms that adapt to
the author's choices — "show seats only when tier is pro or
enterprise", "show the discount-code field only when featured: true",
"show the secondary-CTA group only when the primary CTA is set."
The shape
visibleIf is either a single leaf condition or a composite of
leaves under all / any / not.
- key: seats
type: number
label: Seats
visibleIf:
field: tier
op: in
value: [pro, enterprise]A leaf has three keys:
| Key | Type | Effect |
|---|---|---|
field | string | Path to the field whose value drives visibility. See Path syntax. |
op | string | Comparison operator. See Operators. |
value | any | Value to compare against. Optional for presence operators. |
The field path resolves against the current scope — siblings by default,
parents / root / array-item ancestors via the $prefix syntax.
Path syntax
The CMS's path resolver supports four prefixes:
| Prefix | Resolves against | Use when |
|---|---|---|
key | Sibling at the current group level. | Most common — the controlling field is right next to this one. |
$parent.key | Walk one group level up; chainable. | Inside a nested group, controlling field is one level up. |
$root.path.x | From the page-frontmatter root. | Need a top-level field regardless of nesting depth. |
$item.flag | Nearest enclosing array-item scope. | Inside an array item; controlling field is on the same item. |
Sibling (key)
The default. Use when the controlling field is at the same group level as the dependent one.
- key: tier
type: select
label: Tier
options: [free, pro, enterprise]
- key: seats
type: number
label: Seats
visibleIf:
field: tier # sibling
op: in
value: [pro, enterprise]Walking up ($parent)
Each $parent. segment walks one group level up. Chainable
($parent.$parent.foo).
- key: meta
type: group
label: Meta
fields:
- key: enableSchema
type: boolean
label: Enable schema
- key: schemaContent
type: paragraph
label: Schema content
visibleIf:
field: enableSchema # sibling inside `meta`
op: truthy
- key: extra
type: group
label: Extra
fields:
- key: customSchemaContent
type: paragraph
label: Custom schema content
visibleIf:
field: $parent.meta.enableSchema # walk up to root, descend into meta
op: truthyAbsolute ($root)
Reaches the page-frontmatter root regardless of how deep the current field is. Useful for "the entire site behaves this way when X is set on the root."
- key: blocks
type: array
items:
- $ref: pricing-table
# fields inside the pricing-table block...
# ...one of those fields:
- key: addon
type: text
visibleIf:
field: $root.tier # root frontmatter regardless of array depth
op: eq
value: enterpriseArray-item scope ($item)
The nearest enclosing array-item carries $item. Useful when fields inside
a multi-shape array item want to depend on the item's discriminator or one
of its own siblings.
- key: blocks
type: array
items:
- $ref: media-text
# inside media-text:
- key: layout
type: select
options: [left, right, wide]
- key: wideOptions
type: group
visibleIf:
field: $item.layout
op: eq
value: wide
fields: [...]Operators
| Op | Compares |
|---|---|
eq | Strict equality with value. |
neq | Strict inequality. |
in | value is an array; the field's value is in that array. |
notIn | Inverse of in. |
truthy | JS-truthy (true, non-empty string, non-zero number, …). |
falsy | JS-falsy. |
empty | "", [], null, undefined. |
nonEmpty | Inverse of empty. |
gt / lt | Numeric compare with value. |
matches | value is a regex source string; tests against the field's value. |
The four presence operators (truthy, falsy, empty, nonEmpty) skip
the field resolvability warning the schema validator otherwise emits.
Missing paths legitimately read as "absent" — exactly what truthy /
empty are for.
Composite conditions
Wrap leaves under all / any / not for boolean composition.
all — every sub-condition must be true
visibleIf:
all:
- field: tier
op: eq
value: pro
- field: $root.featureFlags.advancedSeats
op: truthyany — at least one sub-condition must be true
visibleIf:
any:
- field: tier
op: eq
value: pro
- field: tier
op: eq
value: enterprise(For "value is one of N" prefer op: in over any of eqs — it's
shorter and the schema-warnings validator handles it the same way.)
not — invert a sub-condition
visibleIf:
not:
field: tier
op: eq
value: free(For simple negation prefer op: neq over not of eq.)
Composing — all / any / not nest arbitrarily
visibleIf:
all:
- any:
- field: tier
op: eq
value: pro
- field: tier
op: eq
value: enterprise
- not:
field: $root.frozen
op: truthywhenHidden
Controls what happens to the stored value when visibleIf flips false.
- key: discountCode
type: text
label: Discount code
visibleIf:
field: $root.featured
op: truthy
whenHidden: drop # purge value on Publish when hidden| Value | Effect |
|---|---|
keep | Default. The stored value is preserved across visibility flips. |
drop | The value is purged at the YAML boundary on Publish when the field is hidden. |
Use drop when carrying a stale value forward would itself be a data
quality problem — credit-card details that shouldn't survive a payment-method
switch, secret keys, scheduled-publish dates that no longer apply.
Use the default keep when the field's value is intrinsically meaningful
even when hidden (a stored draft, a setting the author might want to flip
back on later).
Edge cases
Missing referenced field
When visibleIf.field references a path that doesn't resolve, the leaf
evaluates undefined — the dependent field stays hidden.
The schema-warnings validator flags this as a severity: warning so
the schema author sees the authoring mistake (typo, renamed sibling,
$parent chain that walked too far). It doesn't block save; the warning
is advisory.
Presence operators (truthy / falsy / empty / nonEmpty) skip the
warning — for them, "missing" is a legitimate signal, not a typo.
Cyclic dependencies
Two fields can hide each other based on a third. Not deadlocked — the evaluator is one-shot per render, no fixpoint required. Just document the chain so future maintainers don't get confused.
Multi-shape array items
Switching shape inside a multi-shape array item changes which fields exist.
If a sibling visibleIf referenced a now-deleted sibling, the dependent
field's "Add value" affordance appears / disappears based on the new
shape's content. Acceptable — the schema editor's per-shape preview
already shows the author what each shape contains.
Hidden fields × history
The History tab guards the per-field "see history" button so it doesn't
appear for fields the author can't currently see. A field that gets a
whenHidden: drop purge still has a paper trail in git — but the
working-copy editor surfaces won't expose it until the visibility flips
back on.
When NOT to use visibleIf
- For "this field doesn't apply to this content type" — declare a
different collection / block schema instead.
visibleIfis for fields that legitimately exist on this content but only apply some of the time. - For "this field is computed from another" — use
computedinstead. - For "this field is hidden from the editor but used by the build
pipeline" — use
hidden: true(always hidden, value still seeded fromdefault) — much simpler than avisibleIf: { ... falsy }.