Building a contact form in Laravel with Filament (step-by-step) | FilaForms                                 [ ![Filaforms Logo](https://filaforms.app/logo.svg)FilaForms

 ](https://filaforms.app)  [ Features ](https://filaforms.app#features) [ Pricing ](https://filaforms.app#pricing) [ Blog ](https://filaforms.app/blog) [ Documentation ](https://docs.filaforms.app)  [ Try Demo ](https://filaforms.app/login) [ Get Started ](https://filaforms.app#pricing)

 [ Features ](https://filaforms.app#features) [ Pricing ](https://filaforms.app#pricing) [ Blog ](https://filaforms.app/blog) [ Documentation ](https://docs.filaforms.app) [ Try Demo ](https://filaforms.app/login) [ Get Started ](https://filaforms.app#pricing)

   ![FilaForms](https://filaforms.app/logo.svg) FilaForms

 TutorialsBuilding a contact form in Laravel with Filament (step-by-step)
===============================================================

 filaforms.app/blog

  [    Back to blog ](https://filaforms.app/blog) [ Tutorials ](https://filaforms.app/blog/category/tutorials)

Building a contact form in Laravel with Filament (step-by-step)
===============================================================

 Manuk Minasyan ·  March 16, 2025  · 7 min read

 Every Laravel web app needs a contact form. It's usually the first [public-facing form](https://filaforms.app/blog/how-to-add-public-facing-forms-to-your-filament-app) you build, and it's deceptively annoying to get right.

This tutorial covers two approaches: building a Laravel contact form manually with Livewire and Filament's form package, and building one with FilaForms. (For a broader look at how the two approaches compare, see [FilaForms vs building from scratch](https://filaforms.app/blog/filaforms-vs-building-forms-from-scratch-in-laravel).) I'll walk through both so you can pick what fits.

What we're building
-------------------

A contact form with:

- Name (required)
- Email (required, validated)
- Subject (optional dropdown)
- Message (required)
- Admin gets an email on submission
- Submitter gets a confirmation email
- Submissions viewable in the Filament admin panel
- Basic spam protection

Nothing exotic. The kind of form you've built ten times already.

Prerequisites
-------------

- Laravel 11+ application
- Filament 4.x or 5.x installed
- Mail configured (Mailgun, SES, SMTP — whatever you use)
- PHP 8.3+

Method 1: Manual build with Livewire
------------------------------------

### Step 1: Database

Create a migration:

```bash
php artisan make:migration create_contact_submissions_table

```

```php
Schema::create('contact_submissions', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email');
    $table->string('subject')->nullable();
    $table->text('message');
    $table->string('ip_address')->nullable();
    $table->timestamps();
});

```

```bash
php artisan migrate

```

Create the model:

```php
// app/Models/ContactSubmission.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ContactSubmission extends Model
{
protected $fillable = [
'name',
'email',
'subject',
'message',
'ip_address',
];
}

```

### Step 2: Livewire component

```bash
php artisan make:livewire ContactForm

```

```php
// app/Livewire/ContactForm.php
namespace App\Livewire;

use App\Mail\ContactAdminNotification;
use App\Mail\ContactConfirmation;
use App\Models\ContactSubmission;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Illuminate\Support\Facades\Mail;
use Livewire\Component;

class ContactForm extends Component implements HasForms
{
    use InteractsWithForms;

    public ?array $data = [];
    public bool $submitted = false;

    public function mount(): void
    {
        $this->form->fill();
    }

    public function form(Form $form): Form
    {
        return $form
            ->schema([
                TextInput::make('name')
                    ->required()
                    ->maxLength(255),

                TextInput::make('email')
                    ->email()
                    ->required()
                    ->maxLength(255),

                Select::make('subject')
                    ->options([
                        'general' => 'General inquiry',
                        'support' => 'Technical support',
                        'billing' => 'Billing question',
                        'partnership' => 'Partnership',
                    ])
                    ->placeholder('Select a subject'),

                Textarea::make('message')
                    ->required()
                    ->rows(5)
                    ->maxLength(5000),
            ])
            ->statePath('data');
    }

    public function submit(): void
    {
        $data = $this->form->getState();

        // Honeypot check (see Blade template)
        if (! empty($this->data['website'] ?? null)) {
            return;
        }

        $submission = ContactSubmission::create([
            ...$data,
            'ip_address' => request()->ip(),
        ]);

        // Notify admin
        Mail::to(config('mail.admin_address'))
            ->queue(new ContactAdminNotification($submission));

        // Confirm to submitter
        Mail::to($data['email'])
            ->queue(new ContactConfirmation($submission));

        $this->submitted = true;
        $this->form->fill();
    }

    public function render()
    {
        return view('livewire.contact-form');
    }
}

```

### Step 3: Blade template

```blade
{{-- resources/views/livewire/contact-form.blade.php --}}

    @if ($submitted)

            Message sent
            We'll get back to you within 24 hours.

    @else

            {{ $this->form }}
        {{-- Honeypot: hidden from humans, bots fill it in --}}
        &lt;div class=&quot;hidden&quot; aria-hidden=&quot;true&quot;&gt;
            &lt;input type=&quot;text&quot; name=&quot;website&quot; wire:model=&quot;data.website&quot; tabindex=&quot;-1&quot; autocomplete=&quot;off&quot;&gt;
        &lt;/div&gt;

        &lt;button type=&quot;submit&quot; class=&quot;mt-4 px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700&quot;&gt;
            Send message
        &lt;/button&gt;
    &lt;/form&gt;
@endif

```

### Step 4: Mailables

```bash
php artisan make:mail ContactAdminNotification --markdown=mail.contact.admin
php artisan make:mail ContactConfirmation --markdown=mail.contact.confirmation

```

```php
// app/Mail/ContactAdminNotification.php
namespace App\Mail;

use App\Models\ContactSubmission;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class ContactAdminNotification extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(public ContactSubmission $submission) {}

    public function envelope(): Envelope
    {
        return new Envelope(
            subject: "New contact: {$this->submission->subject ?? 'General'}",
        );
    }

    public function content(): Content
    {
        return new Content(
            markdown: 'mail.contact.admin',
        );
    }
}

```

```blade
{{-- resources/views/mail/contact/admin.blade.php --}}

# New contact form submission

**From:** {{ $submission->name }} ({{ $submission->email }})
**Subject:** {{ $submission->subject ?? 'Not specified' }}

{{ $submission->message }}

View in admin

```

I'll skip the confirmation mailable — same pattern, different copy.

### Step 5: Filament resource

```bash
php artisan make:filament-resource ContactSubmission --view

```

Then customize the table columns and form schema. TextColumn for name, email, subject. TextColumn for message with a character limit. Created-at column. Maybe a filter for subject. An export action if you want CSV.

Another 50-80 lines of code I won't paste here because you've written it before.

### Step 6: Route

```php
// routes/web.php
Route::get('/contact', function () {
    return view('contact');
});

```

With a simple page view that renders the Livewire component:

```blade
{{-- resources/views/contact.blade.php --}}

```

### Total file count

- 1 migration
- 1 model
- 1 Livewire component
- 1 Blade template (form)
- 1 page view
- 2 mailables
- 2 mail templates
- 1 Filament resource (with table + view page)
- 1 route

That's 10 files for a contact form. Roughly 3-4 hours if you're moving fast, including testing the email flow.

Method 2: FilaForms
-------------------

### Step 1: Install

```bash
composer require filaforms/core
php artisan filaforms:install

```

### Step 2: Register the plugin

```php
// app/Providers/Filament/AdminPanelProvider.php
use FilaForms\Core\FilaFormsPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugins([
FilaFormsPlugin::make(),
]);
}

```

### Step 3: Build the form

Open your Filament admin panel. Click "Fila Forms" in the sidebar.

1. Create a new form, name it "Contact"
2. Drag in a **text** field → label it "Name", mark required
3. Drag in an **email** field → label it "Email", mark required
4. Drag in a **select** field → label it "Subject", add your options
5. Drag in a **textarea** field → label it "Message", mark required
6. Turn on admin email notifications
7. Turn on auto-responder
8. Publish

The form is live at a public URL. Submissions appear in your admin panel with filtering, search, and CSV export. Analytics start tracking immediately. Honeypot spam protection is active by default.

### Total file count

Zero new files. You edited one existing file (the panel provider) and added two lines.

### Time

About 10 minutes, including installation.

Which method should you use?
----------------------------

**Use Method 1 if:**

- You want full control over every aspect of the form
- The submission triggers complex business logic (creates records in multiple tables, calls APIs, runs workflows)
- You're learning Laravel and want to understand the internals
- You have one form in one project and prefer fewer dependencies

**Use Method 2 if:**

- You need the form working today
- You'll have more forms in the future (feedback, support, applications, surveys)
- Non-developers on your team need to create or edit forms
- You want analytics on form performance
- You're tired of writing the same Livewire component for the fifteenth time

Both approaches produce a working contact form. The difference is time: do you want to spend it on form plumbing or on your actual product?

**Resources:**

- [FilaForms documentation](https://docs.filaforms.app)
- [Filament forms package docs](https://filamentphp.com/docs/3.x/forms/getting-started)
- [Laravel Mail documentation](https://laravel.com/docs/mail)

 Related posts
-------------

 [  Tutorials   Jun 16, 2026

 Accessibility for Laravel Forms: WCAG 2.2 in Practice
-------------------------------------------------------

Procurement just asked if your form is WCAG 2.2 AA. Here's the practical checklist — what's automatic, what needs care, and where most Laravel forms quietly fail.

 ](https://filaforms.app/blog/accessibility-for-laravel-forms-wcag-22-in-practice) [  Tutorials   Jun 2, 2026

 GDPR-Compliant Forms in Laravel: A Practical Checklist
--------------------------------------------------------

GDPR compliance isn't a feature you buy — it's a set of habits. Here's the checklist we follow when building forms that collect data from EU users.

 ](https://filaforms.app/blog/gdpr-compliant-forms-laravel-checklist) [  Tutorials   May 22, 2026

 Stop Spam in Laravel Forms: Honeypot, Rate Limits, and Smarter Defaults
-------------------------------------------------------------------------

reCAPTCHA isn't the answer. Or at least, it's not the first answer. Here's the layered approach we use to keep junk out of FilaForms submissions.

 ](https://filaforms.app/blog/stop-spam-laravel-forms-honeypot-rate-limits)

    ![FilaForms Logo](/logo.svg) FilaForms

 Laravel form infrastructure for Filament. Stop rebuilding forms on every project.

 ### Product

 [ Features ](https://filaforms.app#features) [ Documentation ](https://docs.filaforms.app) [ Blog ](https://filaforms.app/blog) [ Pricing ](https://filaforms.app#pricing) [ Contact ](mailto:hello@filaforms.app)

 ### Legal

 [ Terms of Service ](https://filaforms.app/terms-of-service) [ Privacy Policy ](https://filaforms.app/privacy-policy)

  © 2025-2026 FilaForms. All rights reserved.

 [    ](mailto:hello@filaforms.app) [    ](https://x.com/MinasyanManuk)
