Build a Contact Form with Laravel Livewire 3

Build a contact form with Livewire 3 – real-time validation, sending mail with Mailable, honeypot spam protection, and success states. Full code.

Livewire Contact Form

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:submitApp\Http\Livewire namespace, and $rules array instead of #[Validate] attributes.

Comments

comments