S

Conditional Logic

Control field visibility, disabled state, and read-only mode with declarative conditions or functions.

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:

OperatorDescriptionValue Required
equalsStrict equality (===)Yes
notEqualsStrict inequality (!==)Yes
containsString/array includes valueYes
notContainsString/array does not include valueYes
greaterThanNumeric >Yes
lessThanNumeric <Yes
greaterThanOrEqualNumeric >=Yes
lessThanOrEqualNumeric <=Yes
isTrueValue is truthyNo
isFalseValue is falsyNo
isEmptyValue is "", null, undefined, or empty arrayNo
isNotEmptyValue is not emptyNo

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

ApproachSerializableUse Case
booleanYesStatic, never changes
ConditionGroupYesStorable in DB/API, works with visual builder
(values) => booleanNoComplex 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();

  • Multi-Step Forms — Conditional step routing uses the same ConditionGroup system
  • StylingclassNames() method for conditional CSS
  • Layout System — Responsive column spans per breakpoint