Livewire
Add client-side validation to Livewire components automatically with WithClientValidation magic mode, or bind fields manually with x-wire-validate.
Livewire integration has two layers: the PHP trait keeps the server rules central, and the browser adapter lets fields fail fast before a Livewire request leaves the page. The JavaScript adapter supports both Livewire 3 and Livewire 4.
There are two ways to bind validation:
- Magic mode (default): add the
WithClientValidationtrait and define your rules. The payload travels inside the Livewire snapshot, and everywire:modelfield is validated without any Blade changes. - Manual mode: opt out of magic mode and attach rules per field with the
x-wire-validateAlpine directive.
The PHP side of the Livewire integration ships as its own Composer package:
composer require mrpunyapal/client-validation-livewire
It requires the core package, which Composer installs automatically. Everything still lives under the unchanged MrPunyapal\ClientValidation\Livewire namespace, so existing components need no edits.
Magic mode: trait only, no Blade changes
Component
use Livewire\Component;
use MrPunyapal\ClientValidation\Livewire\WithClientValidation;
class CreateUser extends Component
{
use WithClientValidation;
public string $email = '';
public string $password = '';
public string $password_confirmation = '';
public string $role = 'user';
public string $admin_code = '';
protected function rules(): array
{
return [
'email' => 'required|email|unique:users,email',
'password' => 'required|min:8|confirmed',
'role' => 'required|in:user,admin',
'admin_code' => 'required_if:role,admin|string|min:4',
];
}
protected function messages(): array
{
return [
'email.unique' => 'That email address is already registered.',
];
}
protected function validationAttributes(): array
{
return [
'admin_code' => 'administrator code',
];
}
}
Rules may be defined as a $rules property or a rules() method; the same choice applies to $messages/messages() and $validationAttributes/validationAttributes().
Using Livewire's #[Validate] attribute
You do not have to define a rules() method at all. Livewire's #[Validate] property attributes are picked up automatically and merged into the client payload alongside any rules()/property definitions. Custom messages (message:) and attribute labels (as: or attribute:) flow through too:
use Livewire\Attributes\Validate;
use Livewire\Component;
use MrPunyapal\ClientValidation\Livewire\WithClientValidation;
class CreateUser extends Component
{
use WithClientValidation;
#[Validate('required|email|unique:users,email')]
public string $email = '';
#[Validate('required|min:8', as: 'password', message: 'Your password must be at least 8 characters.')]
public string $password = '';
public string $password_confirmation = '';
}
Expected behavior for this component:
- The browser receives
required,email, andajax:unique:users,emailfor the email field, so server-only rules still validate remotely while the rest run instantly. - The password field validates
requiredandmin:8client-side using the custom message frommessage:. - Rules declared through attributes and through
rules()/$rulesare merged; if both define the same field, therules()/property definition wins.
This works anywhere the trait is used, including Livewire form objects.
Blade view
<form wire:submit="save">
<input type="email" wire:model.blur="email">
<input type="password" wire:model.blur="password">
<input type="password" wire:model.blur="password_confirmation">
<select wire:model.change="role">
<option value="user">User</option>
<option value="admin">Administrator</option>
</select>
@if ($role === 'admin')
<input type="text" wire:model.blur="admin_code">
@endif
<button type="submit">Create user</button>
</form>
No directives, no JSON decoding, and no error placeholders are required. That is the entire setup.
Expected behavior
When a component uses the trait and auto_bind_livewire is enabled (the default):
- A component hook serializes a client-safe payload into the snapshot memo under the
clientValidationkey on every dehydrate. Components without the trait are skipped. The payload contains{rules, ajax_rules, messages, attributes, config}built from your rules, whether they come from#[Validate]attributes, arules()method/property, or a mix of both, plus yourmessages()andvalidationAttributes(). - On component init, the browser adapter reads that memo, builds a validator, and binds listeners to every
[wire:model]field. Fields validate on blur; fields with.livemodifiers (or components using alive/inputvalidation mode) also validate while typing, debounced. - Error containers (
data-error="field") are injected next to failing fields and markedwire:ignore, so no markup changes are needed in your view. - Forms with
wire:submitare intercepted in the capture phase before Livewire sees them. Submitting first runs full client-side validation and shows the first error for each failing field. If everything passes, the form's Livewire action is invoked through$wire, so your action runs exactly as a native submit would. - Conditional rules such as
required_ifship in the payload because sibling values are available in the browser, so they evaluate client-side like any other rule. Dependent fields re-validate automatically: editing a confirmation field clears a staleconfirmederror on its sibling, and changing a field referenced byrequired_ifre-evaluates that rule without waiting for a submit. - Server-side rules arrive prefixed with
ajax:(for exampleajax:unique:users,email). The browser routes them to the Laravel endpoint while the remaining rules still apply instantly. See remote rules for details.
Events dispatched
Both events go through $wire.dispatch, so you can listen from Alpine inside the component or from PHP listeners:
| Event | When it fires | Payload |
|---|---|---|
client-validation |
After each field validation | { field, valid, errors } |
client-validation-form |
After submit-time validation of all fields | { valid, errors } |
use Livewire\Attributes\On;
#[On('client-validation-form')]
public function onClientValidationForm(bool $valid): void
{
// Log analytics, toggle UI state, etc.
}
The manual directive flow below additionally dispatches per-field client-validation-error and client-validation-cleared events.
Configuration
Magic mode is controlled by one config option:
| Option | Environment variable | Default | Effect |
|---|---|---|---|
auto_bind_livewire |
CLIENT_VALIDATION_AUTO_BIND_LIVEWIRE |
true |
Registers the component hook and auto-binds fields when Livewire boots |
// config/client-validation.php
'auto_bind_livewire' => env('CLIENT_VALIDATION_AUTO_BIND_LIVEWIRE', true),
Set it to false to disable magic mode entirely: no hook registration, no snapshot injection, no automatic binding. You can also opt out per page by setting the flag before the package script loads:
<script>window.clientValidationConfig = { autoBindLivewire: false };</script>
Manual mode keeps working either way.
Manual mode: x-wire-validate escape hatch
If you prefer explicit control over which fields validate in the browser, use the x-wire-validate Alpine directive instead of relying on automatic binding.
Validate fields in the Blade view
@php($clientRules = json_decode($this->clientRules, true))
<div x-data="{ clientRules: @js($clientRules) }">
<input
wire:model.live="email"
name="email"
x-wire-validate.live="clientRules.email"
>
<span class="validation-error" data-error="email"></span>
</div>
x-wire-validate uses the same trigger model as x-validate: blur by default, .live for debounced typing feedback. The field still keeps its normal wire:model binding.
Use explicit rules when you only need one field
<input
wire:model.blur="email"
name="email"
x-wire-validate="'required|email|unique:users,email'"
>
This is useful when the rules are small or when you are incrementally adopting the package inside an existing component.
How Livewire morphs are handled
Livewire morphs the DOM after every request, which would normally wipe injected error containers or replace input nodes entirely. The adapter handles this in two ways:
- Injected error containers are marked
wire:ignore, so morphs leave them in place between renders. - A
morphedhook re-binds any replaced inputs and forms after each request and re-applies the current error state from the validator. Messages disappear once a field becomes valid and survive round-trips while an error persists.
Related pages
- See configuration for
validation_mode, debounce, AJAX, and styling defaults. - Open filament for the panel-specific helper layer.
- Use troubleshooting when Livewire requests and client validation drift apart.
- Check examples for compact component snippets.