Introduction
Livewire turns form validation into a reactive experience — users see errors as they type, using Laravel's normal validation rules and the familiar $errors variable in Blade.
Key Concepts
#[Validate]attribute: Declares validation rules directly above a public property — the idiomatic Livewire 4 approach.$rulesproperty: The classic alternative — a per-field rules array on the component class, still fully supported.validate()/validateOnly(): Methods that run validation and throw if it fails;validateOnlyscopes validation to a single property.- Form Object: A dedicated class (
make:livewire-form) that encapsulates form fields, rules, and save logic — ideal for complex forms. WithFileUploadstrait: Adds temporary file handling, preview URLs, and progress events to a Livewire component.- Custom messages / attributes:
$messagesand$validationAttributesproperties that override the default validation text.
Real World Context
Traditional PHP forms lose all input on validation failure and only surface errors after a full page reload. Livewire keeps input in memory and validates incrementally, giving users immediate feedback without shipping client-side validation rules that drift from the server's.
Deep Dive
Build powerful forms with real-time validation using Livewire.
Basic Form
php<?php namespace App\Livewire; use App\Models\Post; use Livewire\Component; class CreatePost extends Component { public string $title = ''; public string $body = ''; protected array $rules = [ 'title' => 'required|min:3|max:255', 'body' => 'required|min:10', ]; public function save(): void { $validated = $this->validate(); Post::create($validated); $this->reset(); // Clear form session()->flash('success', 'Post created!'); } public function render() { return view('livewire.create-post'); } }
blade<div> @if (session('success')) <div class="bg-green-100 p-4 mb-4">{{ session('success') }}</div> @endif <form wire:submit="save"> <div class="mb-4"> <label>Title</label> <input type="text" wire:model="title" class="w-full border p-2"> @error('title') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror </div> <div class="mb-4"> <label>Body</label> <textarea wire:model="body" rows="5" class="w-full border p-2"></textarea> @error('body') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror </div> <button type="submit" class="bg-blue-500 text-white px-4 py-2"> Create Post </button> </form> </div>
Real-Time Validation
Validate as the user types:
phpclass CreatePost extends Component { public string $title = ''; public string $body = ''; protected array $rules = [ 'title' => 'required|min:3|max:255', 'body' => 'required|min:10', ]; // Called when any property is updated public function updated(string $propertyName): void { $this->validateOnly($propertyName); } // Or for specific properties public function updatedTitle(): void { $this->validateOnly('title'); } public function save(): void { $this->validate(); // ... } }
blade{{-- Use wire:model.live for real-time validation --}} <input type="text" wire:model.live.debounce.500ms="title">
Form Objects
For complex forms, use Livewire Form Objects:
bashphp artisan make:livewire-form PostForm
php<?php namespace App\Livewire\Forms; use Livewire\Form; use App\Models\Post; class PostForm extends Form { public ?Post $post = null; public string $title = ''; public string $body = ''; public bool $published = false; protected function rules(): array { return [ 'title' => 'required|min:3|max:255', 'body' => 'required|min:10', 'published' => 'boolean', ]; } public function setPost(Post $post): void { $this->post = $post; $this->title = $post->title; $this->body = $post->body; $this->published = $post->published; } public function store(): Post { $this->validate(); return Post::create($this->only(['title', 'body', 'published'])); } public function update(): void { $this->validate(); $this->post->update($this->only(['title', 'body', 'published'])); } }
Using the form:
phpclass ManagePost extends Component { public PostForm $form; public function mount(?Post $post = null): void { if ($post) { $this->form->setPost($post); } } public function save(): void { if ($this->form->post) { $this->form->update(); } else { $this->form->store(); } $this->redirect(route('posts.index')); } public function render() { return view('livewire.manage-post'); } }
blade<form wire:submit="save"> <input type="text" wire:model="form.title"> @error('form.title') <span>{{ $message }}</span> @enderror <textarea wire:model="form.body"></textarea> @error('form.body') <span>{{ $message }}</span> @enderror <label> <input type="checkbox" wire:model="form.published"> Published </label> <button type="submit">Save</button> </form>
File Uploads
phpuse Livewire\WithFileUploads; class UploadPhoto extends Component { use WithFileUploads; public $photo; protected array $rules = [ 'photo' => 'required|image|max:1024', // 1MB max ]; public function save(): void { $this->validate(); $path = $this->photo->store('photos', 'public'); auth()->user()->update(['avatar' => $path]); session()->flash('success', 'Photo uploaded!'); } public function render() { return view('livewire.upload-photo'); } }
blade<form wire:submit="save"> <input type="file" wire:model="photo"> {{-- Preview --}} @if ($photo) <img src="{{ $photo->temporaryUrl() }}" class="w-32 h-32"> @endif @error('photo') <span>{{ $message }}</span> @enderror <button type="submit" wire:loading.attr="disabled"> <span wire:loading.remove>Upload</span> <span wire:loading>Uploading...</span> </button> </form>
Custom Validation Messages
phpprotected array $messages = [ 'title.required' => 'Please enter a title.', 'title.min' => 'The title must be at least :min characters.', 'body.required' => 'Don\'t forget to add content!', ]; protected array $validationAttributes = [ 'title' => 'post title', 'body' => 'post content', ];
Common Pitfalls
- Validating everything on every keystroke — Use
validateOnly($propertyName)in anupdatedhook so one field's edit doesn't re-validate the whole form. - Storing uploaded files without configuring a disk —
store('photos', 'public')needs thepublicdisk registered and symlinked viaphp artisan storage:link. - Forgetting to reset after a successful submit — Users see their old data still in the fields until you call
$this->reset().
Best Practices
- Prefer
#[Validate]attributes for simple forms — Declaring rules inline keeps each field's validation next to its declaration:#[Validate('required|min:3')] public string $title = '';. The old$rulesarray still works but is no longer the idiomatic default. - Use
.debouncewith.live— Live validation is great, but not on every keystroke. 300–500ms is a good balance. - Extract Form Objects for complex forms — Anything with more than 5–6 fields and interdependent rules belongs in a Form Object.
- Always validate server-side, even with real-time feedback — Client-side hints are UX, not security.
Summary
- Declare rules with
#[Validate]attributes (or a legacy$rulesarray), then call$this->validate()in save methods. - Use
validateOnly($field)inupdatedhooks for per-field real-time validation. - Form Objects (
make:livewire-form) encapsulate complex forms. WithFileUploadstrait enables temporary files, preview URLs, and progress tracking.$messagesand$validationAttributescustomize error text.