Last updated: July 2026 — updated for Livewire 3 syntax
A contact form is the perfect first Livewire component: validation without page reloads, no JavaScript to write. This version uses current Livewire 3 conventions (wire:model.blur, attribute validation, the new namespace).
1. Create the component and mailable
php artisan make:livewire ContactForm
php artisan make:mail ContactMessage
2. The component class
<?php
// app/Livewire/ContactForm.php
namespace App\Livewire;
use App\Mail\ContactMessage;
use Illuminate\Support\Facades\Mail;
use Livewire\Attributes\Validate;
use Livewire\Component;
class ContactForm extends Component
{
#[Validate('required|string|min:3|max:80')]
public string $name = '';
#[Validate('required|email')]
public string $email = '';
#[Validate('required|string|min:10|max:2000')]
public string $message = '';
public string $website = ''; // honeypot — humans never fill this
public bool $sent = false;
public function submit(): void
{
if ($this->website !== '') {
$this->sent = true; // silently drop bots
return;
}
$this->validate();
Mail::to(config('mail.from.address'))
->send(new ContactMessage($this->name, $this->email, $this->message));
$this->reset('name', 'email', 'message');
$this->sent = true;
}
public function render()
{
return view('livewire.contact-form');
}
}
3. The Blade view
{{-- resources/views/livewire/contact-form.blade.php --}}
<div>
@if ($sent)
<div class="alert alert-success">Thanks — your message has been sent.</div>
@endif
<form wire:submit="submit">
<input type="text" wire:model.blur="name" placeholder="Your name">
@error('name') <span class="text-danger">{{ $message }}</span> @enderror
<input type="email" wire:model.blur="email" placeholder="Email address">
@error('email') <span class="text-danger">{{ $message }}</span> @enderror
<textarea wire:model.blur="message" rows="5" placeholder="Message"></textarea>
@error('message') <span class="text-danger">{{ $message }}</span> @enderror
<input type="text" wire:model="website" style="display:none" tabindex="-1" autocomplete="off">
<button type="submit" wire:loading.attr="disabled">
<span wire:loading.remove>Send message</span>
<span wire:loading>Sending…</span>
</button>
</form>
</div>
wire:model.blur validates each field the moment the user leaves it (Livewire 2’s .lazy equivalent), wire:loading gives a built-in sending state, and the hidden website field is a honeypot that quietly swallows bot submissions.
4. The mailable
public function __construct(
public string $senderName,
public string $senderEmail,
public string $body,
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Contact form: ' . $this->senderName,
replyTo: [new Address($this->senderEmail, $this->senderName)],
);
}
Setting replyTo to the visitor’s address means you can hit Reply in your inbox directly. Drop the component anywhere with <livewire:contact-form />.
Livewire 2 differences, if your project is older: wire:submit.prevent instead of wire:submit, App\Http\Livewire namespace, and $rules array instead of #[Validate] attributes.
