Conditional Logic
Every field supports three state properties that control its behavior based on other field values: hidden, disabled, and readOnly. Each accepts a boolean, a function, a declarative ConditionGroup, or (for hidden only) responsive breakpoint visibility.
Quick Overview
FormBuilder.create('example')
.addField('plan', (f) =>
f
.type('select')
.label('Plan')
.options([
{ label: 'Free', value: 'free' },
{ label: 'Pro', value: 'pro' },
]),
)
.addField('billing', (f) =>
f
.type('select')
.label('Billing Cycle')
.hidden({
conditions: [{ field: 'plan', operator: 'equals', value: 'free' }],
operator: 'AND',
}),
)
.addField('coupon', (f) =>
f
.type('text')
.label('Coupon Code')
.disabled((values) => values.plan === 'free'),
)
.addStep('main', ['plan', 'billing', 'coupon'])
.build();
State Properties
hidden
Controls whether a field is rendered. When hidden, the field is removed from the DOM and excluded from validation.
// Static boolean
.addField('notes', (f) => f.type('textarea').hidden(true))
// Function — receives all form values
.addField('company', (f) => f.type('text').label('Company')
.hidden((values) => values.accountType !== 'business')
)
// Declarative ConditionGroup
.addField('company', (f) => f.type('text').label('Company')
.hidden({
conditions: [{ field: 'accountType', operator: 'notEquals', value: 'business' }],
operator: 'AND',
})
)
// Responsive breakpoint visibility (CSS-based, field always exists in DOM)
.addField('sidebar', (f) => f.type('html').hidden({
default: 'hidden',
lg: 'visible',
}))
disabled
Prevents interaction with the field. The field remains visible and its value is still submitted.
// Static boolean
.addField('email', (f) => f.type('email').disabled(true))
// Function
.addField('email', (f) => f.type('email')
.disabled((values) => !!values.useSameEmail)
)
// Declarative ConditionGroup
.addField('email', (f) => f.type('email')
.disabled({
conditions: [{ field: 'useSameEmail', operator: 'isTrue' }],
operator: 'AND',
})
)
readOnly
Like disabled, but with read-only styling. The field value is submitted but the user cannot modify it.
// Static boolean
.addField('id', (f) => f.type('text').readOnly(true))
// Function
.addField('total', (f) => f.type('text')
.readOnly((values) => values.status === 'confirmed')
)
// Declarative ConditionGroup
.addField('total', (f) => f.type('text')
.readOnly({
conditions: [{ field: 'status', operator: 'equals', value: 'confirmed' }],
operator: 'AND',
})
)
Condition Operators
The ConditionGroup system uses 12 operators for declarative conditions:
| Operator | Description | Value Required |
|---|---|---|
equals | Strict equality (===) | Yes |
notEquals | Strict inequality (!==) | Yes |
contains | String/array includes value | Yes |
notContains | String/array does not include value | Yes |
greaterThan | Numeric > | Yes |
lessThan | Numeric < | Yes |
greaterThanOrEqual | Numeric >= | Yes |
lessThanOrEqual | Numeric <= | Yes |
isTrue | Value is truthy | No |
isFalse | Value is falsy | No |
isEmpty | Value is "", null, undefined, or empty array | No |
isNotEmpty | Value is not empty | No |
ConditionGroup
A ConditionGroup combines multiple conditions with AND or OR logic:
type ConditionGroup = {
conditions: Condition[];
operator: 'AND' | 'OR';
};
type Condition = {
field: string; // Field name to watch
operator: ConditionOperator;
value?: string | number | boolean | null;
};
AND — All conditions must be true
.hidden({
conditions: [
{ field: 'country', operator: 'equals', value: 'US' },
{ field: 'state', operator: 'isNotEmpty' },
],
operator: 'AND',
})
OR — At least one condition must be true
.disabled({
conditions: [
{ field: 'status', operator: 'equals', value: 'locked' },
{ field: 'role', operator: 'equals', value: 'viewer' },
],
operator: 'OR',
})
Responsive Visibility
The hidden property has a special responsive mode using Tailwind breakpoints. Unlike function/ConditionGroup modes which remove the field from the DOM, responsive visibility uses CSS classes (hidden/block) — the field stays in the DOM at all sizes.
.addField('desktopOnly', (f) => f.type('html')
.value('<p>This content only shows on large screens</p>')
.hidden({
default: 'hidden', // Hidden on mobile
lg: 'visible', // Visible on lg+ screens
})
)
Available breakpoints: default, sm, md, lg, xl, 2xl
Each breakpoint accepts 'visible' or 'hidden'. Breakpoints cascade — setting md: 'visible' means the field stays visible on md, lg, xl, and 2xl unless overridden.
Function vs Declarative
| Approach | Serializable | Use Case |
|---|---|---|
boolean | Yes | Static, never changes |
ConditionGroup | Yes | Storable in DB/API, works with visual builder |
(values) => boolean | No | Complex logic, computed conditions |
Use ConditionGroup when the form configuration comes from a database, API, or the visual form builder — it’s pure JSON and can be stored/transmitted.
Use functions when you need logic that can’t be expressed with the 12 operators — regex matching, API calls, or combining values from multiple fields in complex ways.
Complete Example
const config = FormBuilder.create('registration')
.addField('accountType', (f) =>
f
.type('button-radio')
.label('Account Type')
.options([
{ label: 'Personal', value: 'personal' },
{ label: 'Business', value: 'business' },
])
.required(),
)
.addField('name', (f) => f.type('text').label('Full Name').required())
.addField('company', (f) =>
f
.type('text')
.label('Company Name')
.required()
.hidden({
conditions: [{ field: 'accountType', operator: 'notEquals', value: 'business' }],
operator: 'AND',
}),
)
.addField('taxId', (f) =>
f
.type('text')
.label('Tax ID')
.hidden({
conditions: [{ field: 'accountType', operator: 'notEquals', value: 'business' }],
operator: 'AND',
}),
)
.addField('newsletter', (f) => f.type('checkbox').label('Subscribe to newsletter'))
.addField('frequency', (f) =>
f
.type('select')
.label('Email Frequency')
.options([
{ label: 'Daily', value: 'daily' },
{ label: 'Weekly', value: 'weekly' },
{ label: 'Monthly', value: 'monthly' },
])
.disabled((values) => !values.newsletter),
)
.addStep('main', ['accountType', 'name', 'company', 'taxId', 'newsletter', 'frequency'])
.build();
Related
- Multi-Step Forms — Conditional step routing uses the same
ConditionGroupsystem - Styling —
classNames()method for conditional CSS - Layout System — Responsive column spans per breakpoint