Forms
Form Validation
Rules at three levels — the field, the form and the pipeline — and which of them wins when they disagree.
On this page
- Field-Level Rules
- Type Helpers Are Not Validation
- Unique Values
- Custom Validation Messages
- Form-Level Rules
- Programmatic Validation
- validate()
- getValidationRules()
- Validation in Save Lifecycle
- Standalone Validation (without Livewire)
- Conditional Rules
- Conditioning Helpers
- Live Validation
- Error Display
Wire Forms provides validation at three levels: field-level rules, form-level rules, and programmatic validation via the Core ValidationPipeline.
Field-Level Rules
Each field declares its own validation rules:
TextInput::make('name') ->required() ->maxLength(255) ->rules(['string', 'regex:/^[a-zA-Z\s]+$/']); TextInput::make('email') ->email() ->required() ->unique(); TextInput::make('age') ->numeric() ->rules(['integer', 'min:18', 'max:120']); Select::make('role') ->required() ->rules('in:admin,editor,viewer');
Type Helpers Are Not Validation
->email(), ->numeric(), ->integer(), ->url() and ->tel() are type
presets: they set the HTML input type and inputmode, which picks the phone
keyboard and lets the browser offer its own hint. ->maxLength() and
->minLength() render the maxlength / minlength attributes. None of them
adds a Laravel rule, and none of them survives a request that did not come from
your form — the server has to be told separately what it will accept:
| Method | What it actually does | The rule to add |
|---|---|---|
->email() |
type=email, inputmode=email |
->rules(['email']) |
->numeric() |
type=number, inputmode=decimal |
->rules(['numeric']) |
->integer() |
type=number, inputmode=numeric, step=1 |
->rules(['integer']) |
->url() |
type=url |
->rules(['url']) |
->tel() |
type=tel |
->rules(['regex:…']), or use PhoneInput |
->maxLength(255) |
maxlength="255" |
->rules(['max:255']) |
->minLength(3) |
minlength="3" |
->rules(['min:3']) |
The helpers that are rules are ->required() (and ->requiredWith()), which
prepends required, and ->unique(). Some fields also carry
implicit rules they add on their own, because their state cannot be checked
any other way: MoneyInput validates the amount behind
its formatted text, PhoneInput the number behind its
country prefix, FileUpload the mime types and sizes it
was configured with, and a field with options() an in: constraint over its
own option keys (unless you declared one yourself).
A cleared numeric field does not need a rule to be safe. A
<input type=number>submits''when it is emptied, andTextInputturns that intonullon the way to the record rather than writing an empty string to a numeric column. See Empty Values.
Unique Values
unique() builds Laravel's Rule::unique() from what the form already knows:
the table from the bound model, the column from the field's name, and — in edit
mode — the record being edited is excluded from its own check.
TextInput::make('email')->unique(); // unique:users,email, ignoring this userTextInput::make('email')->unique(ignoreRecord: false); // every row counts, including this oneTextInput::make('tax_id')->unique(column: 'vat_number'); // a field whose name is not the columnTextInput::make('name')->unique(table: 'companies'); // a form with no ->model()
The rule is built when validation runs, not when the schema is declared. That
matters because the record arrives from the form runtime after your form()
method has returned — a rule resolved at declaration time would ignore nothing
on the very form it was written for.
Scope the check further with modifyRuleUsing, which receives the Unique rule:
TextInput::make('email') ->unique(modifyRuleUsing: fn (Unique $rule) => $rule->where('team_id', $this->teamId));
A form with no ->model() has no table to infer, so pass one: without either,
unique() throws a FormConfigurationException naming the field.
Custom Validation Messages
TextInput::make('name') ->required() ->validationMessages([ 'required' => 'Please enter a name.', 'max' => 'Name is too long.', ]);
Form-Level Rules
Add rules at the form level that span multiple fields:
Form::make() ->schema([ TextInput::make('password')->password()->required()->rules(['confirmed']), TextInput::make('password_confirmation')->password()->required()->dehydrated(false), ]) ->validationMessages([ 'password.confirmed' => 'Passwords do not match.', ]);
The confirmation field has a rule and no column behind it, so it is marked
dehydrated(false): it takes part in validation and is dropped before the record
is written. Without that the save would try to set a password_confirmation
attribute on the model and fail on the missing column — see
Save Lifecycle.
Programmatic Validation
validate()
Validates state against all collected rules and returns validated data:
// In a Livewire componentpublic function save(): void{ $data = $this->form->validate(); // $data contains only validated fields // Throws Illuminate\Validation\ValidationException on failure}
getValidationRules()
Inspect the collected rules without validating:
$rules = $this->form->getValidationRules();// ['name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email'], ...]
Validation in Save Lifecycle
When calling $form->save(), validation happens automatically as the first step:
save()├── 1. Validate ← all field + form rules├── 2. mutateDataBeforeSave()├── 3. Plugin hook: form.saving├── 4. beforeSave()├── 5. Persist (create/update)├── 6. Save relationships├── 7. afterSave()├── 8. Plugin hook: form.saved└── 9. Success notification
If validation fails, save() throws ValidationException and steps 2-9 are skipped.
Standalone Validation (without Livewire)
$form = Form::make() ->schema([ TextInput::make('name')->required(), TextInput::make('email')->email()->required(), ]) ->state(['name' => '', 'email' => 'not-an-email']); try { $data = $form->validate();} catch (ValidationException $e) { $errors = $e->errors(); // ['name' => ['The name field is required.'], 'email' => ['The email field must be a valid email address.']]}
Conditional Rules
Rules can use Closures for dynamic validation. A Closure receives the field's
reactive $get / $set accessors, so rules can depend on live sibling state.
The Closure can wrap the whole rule set, or sit inside a rules array as a single
entry:
TextInput::make('company_name') ->required(fn (callable $get) => $get('type') === 'business') ->rules(fn (callable $get) => $get('type') === 'business' ? ['min:2'] : []); // Closures may also be individual entries in a rules array:TextInput::make('slug')->rules([ 'string', fn (callable $get) => $get('type') === 'business' ? 'required' : 'nullable',]);
Conditioning Helpers
Fluent shortcuts express the most common cross-field conditions without writing a Closure. Each compares another field's live value; passing an array matches "is one of".
| Method | Behavior |
|---|---|
->requiredIf('type', 'business') |
required when type equals the value (or is one of an array) |
->requiredUnless('type', 'individual') |
required unless type equals the value |
->requiredWith('company') |
required when company has a non-empty value |
->visibleWhen('type', 'business') |
shown only when type matches |
->hiddenWhen('type', 'individual') |
hidden when type matches |
->disabledWhen('locked', true) |
disabled when locked matches |
Select::make('department') ->visibleWhen('type', 'business') ->requiredIf('type', 'business');
visibleWhen / hiddenWhen / disabledWhen are shared foundation helpers, so
they are also available on columns, filters, and actions. On surfaces without a
live state context they no-op (keep the component visible/enabled).
Hidden fields are skipped during validation, so a required rule on a field the
user cannot currently see never blocks submit.
Live Validation
By default a form validates as a whole on submit. Opt a field into per-field validation during the reactive roundtrip so its error appears (and clears) as the user interacts, without flagging the rest of the form:
TextInput::make('email')->email()->required()->validateLive(); // on each changeTextInput::make('name')->required()->validateOnBlur(); // when focus leaves
validateLive() enables live() and validateOnBlur() enables live.blur binding,
so the server sees the change and refreshes only that field's error bag entry.
Conditioning helpers such as requiredIf() are honoured live, because they read
the current sibling state on each roundtrip. Live validation also works for
fields inside Repeater items — each row's field validates against its own
item path (e.g. data.contacts.0.email).
Live validation checks one field at a time. Rules that compare raw sibling values via Laravel's string syntax (e.g.
required_if:other,value) are best validated on submit; userequiredIf()for the reactive equivalent.
Error Display
Validation errors are automatically bound to Livewire's error bag and displayed next to their respective fields. The state path prefix is applied automatically:
// If statePath('data') and field is TextInput::make('name')// Error key: data.name// Livewire displays: @error('data.name')
No manual error rendering is needed in Blade.