Skip to content

Conditional forms

Fields, sections, tabs and wizard steps can depend on other values of the form. The browser shows and hides them at once, the server evaluates the same conditions again when saving.

use Anvil\Schema\When;
$external = When::field('type')->is('external');
Select::make('type')->options(['internal' => 'Internal', 'external' => 'External'])->required(),
TextInput::make('company')->visibleWhen($external)->requiredWhen($external),
Section::make('Contract')->visibleWhen($external)->schema([
DateInput::make('signed_on'),
]),
Wizard::make()->steps([
Step::make('Contact')->schema([...]),
Step::make('Approval')->visibleWhen(When::field('amount')->greaterThan(1000))->schema([...]),
]),
Option On Meaning
visibleWhen($condition) fields, Grid, Section, Tab, Step and own layouts shown only while the condition holds
requiredWhen($condition) fields required only while the condition holds
keepWhenHidden() fields keeps the stored value while the field is hidden
Condition Holds when
When::field('a')->is('x') the value is x; for a list of values (CheckboxList, TagsInput) it contains x; true and false test a Toggle
->isNot('x') the opposite of is()
->in(['x', 'y']), ->notIn([...]) the value is one of them, or none of them
->filled(), ->empty() there is a value, or none. A Toggle that is off is filled: use is(true)
->greaterThan(10), ->lessThan(10) compared as numbers, dates as '2026-12-24'. An empty value never holds
->atLeast(10), ->atMost(10) the same, including the value
When::all($a, $b), When::any($a, $b), When::not($a) combined conditions

Conditions are data and no closures: toArray() and When::fromArray() convert them, and the browser evaluates the same definition as the server.

A BelongsTo sends a UUID to the browser and stores an id. Only test it with filled() and empty().

  • They are not validated. Whatever is sent for them is ignored.
  • They are saved as empty. A value that nobody sees should not stay and come back later, so the column is set to null. Its column must accept null, or the field uses keepWhenHidden().
  • Files of a hidden FileUpload are deleted, unless it uses keepWhenHidden().
  • Virtual fields have no value while hidden.
  • Other conditions see a hidden field as empty. Hiding a field also hides what depends on it.
  • Detail pages leave hidden parts out, evaluated with the stored values.

Switching a field back and forth in the browser does not lose what was typed. The value is only cleared when the form is saved.

The values are compared as they are stored: 1.500,00 in a German form is the number 1500, a date is 2026-12-24.

Fields the user can change use the sent value. All other fields (disabled(), disabledOn(), hiddenOn()) use the stored value, or their default on create, so a condition on them cannot be faked by sending another value.

Conditions apply to resource endpoints as well. A hidden field is ignored, a required field missing gives a validation error. A change uses the stored value for the fields it does not send, so changing type alone can make company required. The OpenAPI description lists the conditions as x-visible-when and x-required-when.

The options of a Select can depend on the value of another field, as a list per value or as a closure:

Select::make('category')->options(['hardware' => 'Hardware', 'software' => 'Software']),
// A list per value: goes to the browser as data, no request.
Select::make('subcategory')->optionsFrom('category', [
'hardware' => ['laptop' => 'Laptop', 'phone' => 'Phone'],
'software' => ['licence' => 'Licence'],
]),
// A closure: runs on the server, the browser loads the options.
Select::make('room')->optionsFrom('site', fn (mixed $site): array => $this->roomsOf($site)),

Records of another model use BelongsTo::dependsOn(). The closure restricts the query, and the browser only sees UUIDs. It also works with searchable():

BelongsTo::make('company_id')->relationship(CompanyModel::class, 'name'),
BelongsTo::make('contact_id')->relationship(ContactModel::class, 'name')->searchable()
->dependsOn('company_id', fn (Model $contacts, mixed $company) => $contacts->where('company_id', $company)),
  • The closure gets the stored value of the other field: for a BelongsTo its internal id, resolved on the server. It is not called without a value, then there are no options.
  • The server checks on saving that the value is an option for the other field.
  • Changing the other field empties this one. Fields that depend on it follow.
  • Through the API, changing only the other field is refused if the stored value does not fit any more. OpenAPI names the field as x-options-from.
  • A Radio cannot depend on another field. Use a Select.

These throw a LogicException, or an InvalidArgumentException for a repeater:

  • a condition on a field that is not in the form,
  • visibility conditions that depend on each other,
  • a condition on a field inside a repeater row (a repeater as a whole can have one),
  • options that depend on an unknown field, or on each other,
  • an editable column on a field with a condition or dependent options, or on a field that those depend on.

A layout with nested components must return them from children(), otherwise the conditions inside it are not found. make:anvil-layout generates this method.