Muhammad Yasir Amin

Loading website…

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

Laravel 13 Livewire 4 Multi-Step Form Wizard with Validation per Step

🛠️ Laravel Tutorials5 min read

Laravel 13 Livewire 4 Multi-Step Form Wizard with Validation per Step

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineer•Nov 12, 2026
Laravel 13 Livewire 4 Multi-Step Form Wizard with Validation per Step

A multi-step form wizard in Livewire keeps the current step number and all field values as public properties of one component. The Next button validates only the fields of the current step before moving on, and the final button validates everything and saves. This tutorial builds a three-step sign-up wizard for Laravel 13 with a Livewire 4 single-file component.

Targets: Laravel 13 (PHP 8.3 to 8.5) and Livewire 4. The component uses the single-file format that the Livewire 4 documentation shows: a PHP class and its Blade template in the same file.

What will the wizard do?

  • Step 1: name and email.
  • Step 2: company and plan.
  • Step 3: review and confirm.

At the end it saves a record to a leads table. You can replace that with any model you like.

Step 1: Create the model

Code
php artisan make:model Lead -m
PHP
Schema::create('leads', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email');
    $table->string('company')->nullable();
    $table->string('plan');
    $table->timestamps();
});
PHP
// app/Models/Lead.php
protected $fillable = ['name', 'email', 'company', 'plan'];

Run php artisan migrate and make sure Livewire is installed (composer require livewire/livewire).

Step 2: Create the wizard component

Create the file resources/views/components/⚡signup-wizard.blade.php (you can also try php artisan make:livewire signup-wizard, which creates a component in the format your Livewire version uses):

JavaScript
<?php

use App\Models\Lead;
use Livewire\Component;

new class extends Component {
    public int $step = 1;

    public string $name = '';
    public string $email = '';
    public string $company = '';
    public string $plan = 'basic';

    public bool $saved = false;

    protected function rulesForStep(int $step): array
    {
        return match ($step) {
            1 => [
                'name' => ['required', 'string', 'max:255'],
                'email' => ['required', 'email', 'max:255'],
            ],
            2 => [
                'company' => ['nullable', 'string', 'max:255'],
                'plan' => ['required', 'in:basic,pro,team'],
            ],
            default => [],
        };
    }

    public function next(): void
    {
        $this->validate($this->rulesForStep($this->step));

        $this->step = min($this->step + 1, 3);
    }

    public function previous(): void
    {
        $this->step = max($this->step - 1, 1);
    }

    public function save(): void
    {
        $this->validate($this->rulesForStep(1) + $this->rulesForStep(2));

        Lead::create($this->only(['name', 'email', 'company', 'plan']));

        $this->saved = true;
    }
};
?>

<div>
    @if ($saved)
        <p>Thank you, {{ $name }}. Your details were saved.</p>
    @else
        <p>Step {{ $step }} of 3</p>

        @if ($step === 1)
            <input type="text" wire:model="name" placeholder="Name">
            @error('name') <span>{{ $message }}</span> @enderror

            <input type="email" wire:model="email" placeholder="Email">
            @error('email') <span>{{ $message }}</span> @enderror
        @elseif ($step === 2)
            <input type="text" wire:model="company" placeholder="Company (optional)">
            @error('company') <span>{{ $message }}</span> @enderror

            <select wire:model="plan">
                <option value="basic">Basic</option>
                <option value="pro">Pro</option>
                <option value="team">Team</option>
            </select>
            @error('plan') <span>{{ $message }}</span> @enderror
        @else
            <ul>
                <li>Name: {{ $name }}</li>
                <li>Email: {{ $email }}</li>
                <li>Company: {{ $company ?: '-' }}</li>
                <li>Plan: {{ $plan }}</li>
            </ul>
        @endif

        <div>
            @if ($step > 1)
                <button type="button" wire:click="previous">Back</button>
            @endif

            @if ($step < 3)
                <button type="button" wire:click="next">Next</button>
            @else
                <button type="button" wire:click="save">Confirm and save</button>
            @endif
        </div>
    @endif
</div>

Display it in any Blade view with <livewire:signup-wizard />.

How does it work?

  • State: all values live in public properties, so Livewire keeps them between requests and across steps.
  • Validation per step: rulesForStep() returns the rules for one step. next() validates only those fields, so the user is not blocked by fields from steps they have not seen.
  • Final check: save() validates the rules of steps 1 and 2 again. Never rely on step validation alone, because a user can change a public property value in the browser.
  • Safe saving: $this->only([...]) picks the listed properties, so only the intended data goes into Lead::create().

How do I show a progress bar?

HTML / XML
<div style="background: #eee; height: 8px;">
    <div style="background: #0d6efd; height: 8px; width: {{ ($step / 3) * 100 }}%;"></div>
</div>

Add it above the step text. The width changes whenever the step changes.

How do I check that it works?

  1. Click Next on step 1 with empty fields. You should see the validation messages and stay on step 1.
  2. Fill the fields and move to step 2, then use Back and confirm your values are still there.
  3. Reach step 3, click Confirm and check the leads table for the new row.

Common mistakes

  • Validating all fields on every Next: users get errors for fields they cannot see yet. Validate by step.
  • Skipping the final validation: public properties can be tampered with. Validate again in save().
  • Using wire:model.live everywhere: it sends a request on each keystroke. Plain wire:model sends values with the next action, which is lighter.
  • Putting secrets in public properties: public properties are visible to the browser.
  • Following Livewire 3 file layout: Livewire 3 keeps the class in app/Livewire and the view separate. The logic is the same.

FAQ

Can the wizard keep progress after a page refresh?

You can store the step and values in the session or the database, or use Livewire's URL binding for the step number. Do not put sensitive data in the URL.

Can I split steps into child components?

Yes, for large wizards. Keep the shared state and the final save in the parent and pass data down.

Can I upload files in a step?

Yes. Livewire supports file uploads with its WithFileUploads trait. See the Livewire documentation for the current usage.

References

Article Index & Topic Tags
LaravelValidationFormsLivewire

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.