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.