Skip to content

Multi-Step Wizards in Laravel: Livewire + Blade Components Guide

1 min read
Multi-Step Wizards in Laravel: Livewire + Blade Components Guide

Multi-step wizards are essential for breaking down complex forms into manageable chunks, improving user experience and completion rates. In this guide, we'll explore how to build elegant, interactive wizards using Livewire's reactive state management combined with polished Blade components.

Why Multi-Step Wizards Matter

Long forms can be overwhelming and lead to high abandonment rates. Multi-step wizards solve this by:

  • Reducing cognitive load by presenting information in digestible chunks
  • Improving completion rates through psychological commitment and progress visualization
  • Enabling better validation with step-specific error handling
  • Creating smoother user flows for complex processes like onboarding or checkout

Building the Foundation with Livewire

Let's start by creating a Livewire component that manages our wizard state. We'll build a user registration wizard with three steps: personal information, account setup, and preferences.

<?php

namespace App\Livewire;

use Livewire\Component;
use App\Models\User;
use Illuminate\Support\Facades\Hash;

class RegistrationWizard extends Component
{
    public $currentStep = 1;
    public $totalSteps = 3;
    
    // Step 1: Personal Information
    public $firstName = '';
    public $lastName = '';
    public $email = '';
    public $phone = '';
    
    // Step 2: Account Setup
    public $username = '';
    public $password = '';
    public $passwordConfirmation = '';
    
    // Step 3: Preferences
    public $newsletter = false;
    public $notifications = true;
    public $theme = 'light';
    
    protected $rules = [
        'firstName' => 'required|min:2',
        'lastName' => 'required|min:2',
        'email' => 'required|email|unique:users',
        'phone' => 'required|regex:/^[0-9+\-\s]+$/',
        'username' => 'required|min:3|unique:users',
        'password' => 'required|min:8|confirmed',
        'newsletter' => 'boolean',
        'notifications' => 'boolean',
        'theme' => 'required|in:light,dark',
    ];
    
    public function nextStep()
    {
        $this->validateCurrentStep();
        
        if ($this->currentStep < $this->totalSteps) {
            $this->currentStep++;
        }
    }
    
    public function previousStep()
    {
        if ($this->currentStep > 1) {
            $this->currentStep--;
        }
    }
    
    public function validateCurrentStep()
    {
        if ($this->currentStep === 1) {
            $this->validate([
                'firstName' => $this->rules['firstName'],
                'lastName' => $this->rules['lastName'],
                'email' => $this->rules['email'],
                'phone' => $this->rules['phone'],
            ]);
        } elseif ($this->currentStep === 2) {
            $this->validate([
                'username' => $this->rules['username'],
                'password' => $this->rules['password'],
                'passwordConfirmation' => 'required|same:password',
            ]);
        }
    }
    
    public function submit()
    {
        $this->validate();
        
        User::create([
            'first_name' => $this->firstName,
            'last_name' => $this->lastName,
            'email' => $this->email,
            'phone' => $this->phone,
            'username' => $this->username,
            'password' => Hash::make($this->password),
            'newsletter' => $this->newsletter,
            'notifications' => $this->notifications,
            'theme' => $this->theme,
        ]);
        
        session()->flash('message', 'Registration completed successfully!');
        return redirect()->route('dashboard');
    }
    
    public function render()
    {
        return view('livewire.registration-wizard');
    }
}

Creating the Wizard Interface

Now let's build the Blade template that brings our wizard to life with beautiful, accessible components:

<div class="max-w-2xl mx-auto p-6">
    {{-- Progress Indicator --}}
    <div class="mb-8">
        <div class="flex items-center justify-between mb-4">
            @for ($i = 1; $i <= $totalSteps; $i++)
                <div class="flex items-center {{ $i < $totalSteps ? 'flex-1' : '' }}">
                    <x-aura::badge 
                        :variant="$currentStep >= $i ? 'primary' : 'secondary'"
                        class="w-8 h-8 rounded-full flex items-center justify-center"
                    >
                        {{ $i }}
                    </x-aura::badge>
                    
                    @if ($i < $totalSteps)
                        <div class="flex-1 h-1 mx-4 {{ $currentStep > $i ? 'bg-primary-500' : 'bg-gray-300' }} rounded"></div>
                    @endif
                </div>
            @endfor
        </div>
        
        <div class="text-center">
            <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
                @if ($currentStep === 1)
                    Personal Information
                @elseif ($currentStep === 2)
                    Account Setup
                @else
                    Preferences
                @endif
            </h2>
            <p class="text-gray-600 dark:text-gray-400">
                Step {{ $currentStep }} of {{ $totalSteps }}
            </p>
        </div>
    </div>
    
    {{-- Step Content --}}
    <x-aura::card class="mb-6">
        <form wire:submit.prevent="{{ $currentStep === $totalSteps ? 'submit' : 'nextStep' }}">
            @if ($currentStep === 1)
                <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
                    <x-aura::input
                        wire:model="firstName"
                        label="First Name"
                        placeholder="Enter your first name"
                        required
                    />
                    
                    <x-aura::input
                        wire:model="lastName"
                        label="Last Name"
                        placeholder="Enter your last name"
                        required
                    />
                    
                    <x-aura::input
                        wire:model="email"
                        type="email"
                        label="Email Address"
                        placeholder="[email protected]"
                        class="md:col-span-2"
                        required
                    />
                    
                    <x-aura::input
                        wire:model="phone"
                        type="tel"
                        label="Phone Number"
                        placeholder="+1 (555) 123-4567"
                        class="md:col-span-2"
                        required
                    />
                </div>
            @elseif ($currentStep === 2)
                <div class="space-y-4">
                    <x-aura::input
                        wire:model="username"
                        label="Username"
                        placeholder="Choose a unique username"
                        required
                    />
                    
                    <x-aura::input
                        wire:model="password"
                        type="password"
                        label="Password"
                        placeholder="Create a secure password"
                        required
                    />
                    
                    <x-aura::input
                        wire:model="passwordConfirmation"
                        type="password"
                        label="Confirm Password"
                        placeholder="Confirm your password"
                        required
                    />
                </div>
            @else
                <div class="space-y-6">
                    <div class="space-y-4">
                        <x-aura::toggle
                            wire:model="newsletter"
                            label="Subscribe to Newsletter"
                            description="Receive updates about new features and announcements"
                        />
                        
                        <x-aura::toggle
                            wire:model="notifications"
                            label="Enable Notifications"
                            description="Get notified about important account activities"
                        />
                    </div>
                    
                    <x-aura::select
                        wire:model="theme"
                        label="Preferred Theme"
                        :options="[
                            'light' => 'Light Mode',
                            'dark' => 'Dark Mode'
                        ]"
                        placeholder="Choose your theme"
                    />
                </div>
            @endif
        </form>
    </x-aura::card>
    
    {{-- Navigation Buttons --}}
    <div class="flex justify-between">
        <x-aura::button
            variant="secondary"
            wire:click="previousStep"
            :disabled="$currentStep === 1"
        >
            Previous
        </x-aura::button>
        
        <x-aura::button
            type="submit"
            wire:click="{{ $currentStep === $totalSteps ? 'submit' : 'nextStep' }}"
            class="ml-auto"
        >
            {{ $currentStep === $totalSteps ? 'Complete Registration' : 'Next Step' }}
        </x-aura::button>
    </div>
</div>

Advanced Wizard Features

Dynamic Step Validation

Enhance your wizard with real-time validation feedback:

public function updatedEmail()
{
    $this->validateOnly('email');
}

public function updatedUsername()
{
    $this->validateOnly('username');
}

public function getStepValidationProperty()
{
    $stepFields = [
        1 => ['firstName', 'lastName', 'email', 'phone'],
        2 => ['username', 'password'],
        3 => ['theme']
    ];
    
    $currentFields = $stepFields[$this->currentStep] ?? [];
    
    foreach ($currentFields as $field) {
        if (empty($this->$field)) {
            return false;
        }
    }
    
    return true;
}

Step Navigation with URL Updates

Make your wizard bookmarkable by syncing the current step with the URL:

use Livewire\Attributes\Url;

class RegistrationWizard extends Component
{
    #[Url]
    public $currentStep = 1;
    
    public function mount($step = 1)
    {
        $this->currentStep = max(1, min($step, $this->totalSteps));
    }
}

Conditional Step Logic

Sometimes you need to show different steps based on user input:

public function getVisibleStepsProperty()
{
    $steps = [1, 2, 3]; // Base steps
    
    // Add business verification step for business accounts
    if ($this->accountType === 'business') {
        array_splice($steps, 2, 0, [4]); // Insert step 4 after step 2
    }
    
    return $steps;
}

public function nextStep()
{
    $this->validateCurrentStep();
    
    $visibleSteps = $this->getVisibleStepsProperty();
    $currentIndex = array_search($this->currentStep, $visibleSteps);
    
    if ($currentIndex !== false && $currentIndex < count($visibleSteps) - 1) {
        $this->currentStep = $visibleSteps[$currentIndex + 1];
    }
}

Best Practices for Production Wizards

Save Progress Automatically

Implement auto-save functionality to prevent data loss:

public function updated($propertyName)
{
    $this->saveProgress();
}

public function saveProgress()
{
    session()->put('wizard_progress', [
        'step' => $this->currentStep,
        'data' => $this->only([
            'firstName', 'lastName', 'email', 'phone',
            'username', 'newsletter', 'notifications', 'theme'
        ])
    ]);
}

Handle Large Datasets

For wizards with file uploads or large amounts of data, consider using temporary storage:

use Livewire\WithFileUploads;

class RegistrationWizard extends Component
{
    use WithFileUploads;
    
    public $avatar;
    
    public function updatedAvatar()
    {
        $this->validate([
            'avatar' => 'image|max:2048'
        ]);
        
        // Store temporarily
        $path = $this->avatar->store('temp-avatars');
        session()->put('wizard.avatar_path', $path);
    }
}

Conclusion

Multi-step wizards significantly improve user experience for complex forms and processes. By combining Livewire's reactive capabilities with well-designed Blade components, you can create smooth, intuitive workflows that guide users through even the most complex tasks.

The key to successful wizards lies in thoughtful step organization, clear progress indication, robust validation, and graceful error handling. With these foundations in place, your Laravel applications will provide users with delightful, conversion-optimized experiences that keep them engaged from start to finish.

Remember to test your wizards thoroughly across different devices and user scenarios, and always provide clear feedback about progress and next steps. Your users will appreciate the thoughtful design, and you'll see improved completion rates as a result.