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([...]),]),Options
Section titled “Options”| 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 |
Conditions
Section titled “Conditions”| 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().
What happens to hidden fields
Section titled “What happens to hidden fields”- 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 acceptnull, or the field useskeepWhenHidden(). - Files of a hidden
FileUploadare deleted, unless it useskeepWhenHidden(). - 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.
Which values a condition sees
Section titled “Which values a condition sees”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.
Options that depend on another field
Section titled “Options that depend on another field”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
BelongsToits 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
Radiocannot depend on another field. Use aSelect.
Errors of the developer
Section titled “Errors of the developer”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.
Own layouts
Section titled “Own layouts”A layout with nested components must return them from children(), otherwise
the conditions inside it are not found. make:anvil-layout generates this
method.