Conditional Fields
Show a field only when other fields have certain values. Hidden fields are not validated and not saved.
Long forms are easier when editors only see what applies. conditionalRules makes a field appear when rules about other fields match.
import { and, or } from 'nextjs-cms/core/fields'A first example
const linkType = selectField({
name: 'link_type',
required: true,
defaultValue: 'internal',
options: [
{ value: 'internal', label: 'A page on this site' },
{ value: 'external', label: 'An external URL' },
],
})
const page = selectField({
name: 'page_id',
section: pagesSection,
required: true,
conditionalRules: [{ field: linkType, condition: 'equals', value: 'internal' }],
})
const url = textField({
name: 'url',
required: true,
conditionalRules: [{ field: linkType, condition: 'equals', value: 'external' }],
})Editors see either the page picker or the URL box, and each is required only while visible.
Rule shape
{
field: FieldConfig // the controlling field (the object, not its name)
condition: Condition // see below
value: string | number | boolean | null
}| Condition | Matches when the controlling field's value... |
|---|---|
equals | equals value |
notEquals | differs from value (also true when empty) |
contains | contains value as a substring (case-sensitive) |
notContains | doesn't contain value (also true when empty) |
greaterThan | is a number > value |
lessThan | is a number < value |
greaterThanOrEquals | is a number >= value |
lessThanOrEquals | is a number <= value |
Semantics worth knowing:
- Values are compared as strings (
1equals'1'), except the four numeric conditions, which require both sides to be numbers and are false otherwise. - When the controlling field is empty, only the negative conditions (
notEquals,notContains) match. - For
checkboxFieldcontrollers, compare withtrueorfalse. containsworks well with multi-selects and tags, whose values are comma-separated:{ field: features, condition: 'contains', value: 'pool' }.
One rule vs. several
A single rule is a one-element array. Two or more rules must be wrapped in and() or or(). A bare multi-rule array is a type error, so the intent is always explicit:
// One rule
conditionalRules: [{ field: type, condition: 'equals', value: 'event' }]
// All must match
conditionalRules: and(
{ field: type, condition: 'equals', value: 'event' },
{ field: isOnline, condition: 'equals', value: false },
)
// At least one must match
conditionalRules: or(
{ field: status, condition: 'equals', value: 'scheduled' },
{ field: status, condition: 'equals', value: 'published' },
)Groups don't nest: and() and or() take rules, not other groups. When you need "A and (B or C)", restructure the condition. Often a single contains or notEquals rule expresses it, or an extra select field makes the intent explicit.
What "hidden" means
The same evaluator runs in the browser (to show/hide) and on the server (to save), so a field is never shown but ignored, or hidden but saved:
| Visible (rules match) | Hidden (rules don't match) | |
|---|---|---|
| Rendered | ✓ | ✗ |
required enforced | ✓ | ✗ |
| Validated (length, range, file type...) | ✓ | ✗ |
| Written to the database | ✓ | ✗ (the column keeps its previous value) |
| Column | Nullable, even with required: true | Nullable (hidden fields have no value) |
Patterns
Show extra fields for one option
const type = selectField({
name: 'type',
required: true,
options: [
{ value: 'article', label: 'Article' },
{ value: 'video', label: 'Video' },
{ value: 'podcast', label: 'Podcast' },
],
})
const body = richTextField({
name: 'body',
required: true,
conditionalRules: [{ field: type, condition: 'equals', value: 'article' }],
})
const videoUrl = textField({
name: 'video_url',
required: true,
conditionalRules: [{ field: type, condition: 'equals', value: 'video' }],
})
const audio = documentField({
name: 'audio_transcript',
fileType: ['pdf', 'txt'],
conditionalRules: [{ field: type, condition: 'equals', value: 'podcast' }],
})Toggle a whole block with a checkbox
import type { ConditionalRulesConfig } from 'nextjs-cms/core/types'
const hasCta = checkboxField({ name: 'has_cta', label: 'Show a call-to-action' })
const whenCta: ConditionalRulesConfig = [{ field: hasCta, condition: 'equals', value: true }]
const ctaLabel = textField({ name: 'cta_label', required: true, conditionalRules: whenCta })
const ctaUrl = textField({ name: 'cta_url', required: true, conditionalRules: whenCta })Thresholds
const price = numberField({ name: 'price', format: 'float', minValue: 0, maxValue: 1_000_000 })
const shippingNote = textAreaField({
name: 'free_shipping_note',
conditionalRules: [{ field: price, condition: 'greaterThanOrEquals', value: 100 }],
})Depend on a tag or multi-select
const amenities = selectMultipleField({
name: 'amenities',
options: [
{ value: 'pool', label: 'Pool' },
{ value: 'gym', label: 'Gym' },
{ value: 'parking', label: 'Parking' },
],
})
const poolHours = textField({
name: 'pool_hours',
conditionalRules: [{ field: amenities, condition: 'contains', value: 'pool' }],
})Don't make a field depend on itself, and avoid chains where a hidden field controls another field. If A hides B, rules that read B see an empty value.
Good to know
- The controlling field must be in the same section.
- Conditional fields work inside field groups, and across groups.
- Changing rules doesn't change the table (except nullability when you add or remove the first rule on a required field), so a refresh is usually enough.