Introduction
The Wizard component provides a step-by-step interface for multi-step forms and workflows. With clean visual progress indicators and flexible content areas, it's perfect for onboarding flows, multi-page forms, and guided processes.
Installation
Use the Sheaf artisan command to install the wizard:
php artisan sheaf:install wizard
Once installed, you can use
<x-ui.wizard />,<x-ui.wizard.steps />,<x-ui.wizard.step />, and<x-ui.wizard.body />in any Blade view, and theWizard,Step,HasWizard, andWizardSynthesizerclasses are available in your application.
Then register the synthesizer in your service provider so Livewire knows how to serialize the Wizard object between requests:
use Livewire\Livewire; use App\Livewire\Synthesizers\WizardSynthesizer; public function boot(): void { Livewire::propertySynthesizer(WizardSynthesizer::class); }
The wizard's backend classes are currently designed for Livewire. Using them outside of Livewire requires handling state serialization yourself — a standalone guide for that is coming soon. For a complete working example, see the Implementation Guide below.
Basic Usage
<x-ui.wizard variant="default"> <x-ui.wizard.steps color="sky"> <x-ui.wizard.step :active="false" :completed="true" :label="1"> <x-ui.heading>Personal Info</x-ui.heading> <x-ui.text class="opacity-70">Name and email</x-ui.text> </x-ui.wizard.step> <x-ui.wizard.step :active="false" :completed="true" :label="2"> <x-ui.heading>Account Details</x-ui.heading> <x-ui.text class="opacity-70">Username and password</x-ui.text> </x-ui.wizard.step> <x-ui.wizard.step :active="true" :completed="false" :label="3"> <x-ui.heading>Verification</x-ui.heading> <x-ui.text class="opacity-70">Confirm your email</x-ui.text> </x-ui.wizard.step> </x-ui.wizard.steps> <x-ui.wizard.body> <div class="p-6"> <x-ui.text>Step 3 content goes here</x-ui.text> </div> </x-ui.wizard.body> </x-ui.wizard>
Variants
The wizard component supports two visual variants, in addition to the default variant above there is minimal variant:
Minimal Variant
A cleaner design with connecting lines between steps:
<x-ui.wizard variant="minimal"> <x-ui.wizard.steps> <!-- Steps --> </x-ui.wizard.steps> <x-ui.wizard.body> <!-- Content --> </x-ui.wizard.body> </x-ui.wizard>
Color Customization
Customize the wizard's accent color using the color prop on wizard.steps:
<x-ui.wizard> <x-ui.wizard.steps color="purple"> <x-ui.wizard.step :active="true" :label="1"> <x-ui.heading>Step 1</x-ui.heading> </x-ui.wizard.step> <!-- More steps --> </x-ui.wizard.steps> </x-ui.wizard> <x-ui.wizard variant="minimal"> <x-ui.wizard.steps color="teal"> <x-ui.wizard.step :active="true" :label="1"> <x-ui.heading>Step 1</x-ui.heading> </x-ui.wizard.step> <!-- More steps --> </x-ui.wizard.steps> </x-ui.wizard>
Available colors: slate, neutral, zinc, stone, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose
Custom Icons
Use custom icons instead of numbers for step labels:
<x-ui.wizard.step icon="user" :active="true"> </x-slot:icon> <x-ui.heading>Profile</x-ui.heading> <x-ui.text>Basic information</x-ui.text> </x-ui.wizard.step>
You can also customize the completed icon:
<x-ui.wizard.step :completed="true" completedIcon="check-badge"> <x-ui.heading>Completed</x-ui.heading> </x-ui.wizard.step>
Implementation Guide
This guide shows you how to build a fully functional multi-step wizard using Livewire. We'll create a user onboarding flow with account creation, profile setup, and preferences configuration.
Overview
We'll build a wizard that:
- Tracks progress through three steps: Account, Profile, Preferences
- Validates each step before allowing progression
- Allows skipping optional steps
- Collects all data and saves atomically at the end
Step 1: Create Form Objects
Create a separate Livewire Form class for each step to own its fields and validation rules.
Account Form — app/Livewire/Forms/AccountForm.php:
<?php namespace App\Livewire\Forms; use Livewire\Attributes\Validate; use Livewire\Form; class AccountForm extends Form { #[Validate('required|min:3')] public string $username = ''; #[Validate('required|email')] public string $email = ''; }
Profile Form — app/Livewire/Forms/ProfileForm.php:
<?php namespace App\Livewire\Forms; use Livewire\Attributes\Validate; use Livewire\Form; class ProfileForm extends Form { #[Validate('required|min:3')] public string $first_name = ''; #[Validate('required|min:3')] public string $last_name = ''; }
Preferences Form — app/Livewire/Forms/PreferencesForm.php:
<?php namespace App\Livewire\Forms; use Livewire\Form; class PreferencesForm extends Form { public bool $email_notifications = true; public bool $push_notifications = false; public bool $sms_notifications = false; }
Note: Steps with no validation rules (like Preferences) will advance freely. The wizard handles this automatically.
Step 2: Create the Wizard Component
Use the HasWizard trait and implement setupWizard() to define your steps:
<?php namespace App\Livewire; use App\View\Components\Step; use App\View\Components\Wizard; use App\Livewire\Forms\AccountForm; use App\Livewire\Forms\ProfileForm; use App\Livewire\Forms\PreferencesForm; use Illuminate\Support\Facades\DB; use Illuminate\View\View; use Livewire\Component; use App\Livewire\Concerns\HasWizard; class UserOnboarding extends Component { use HasWizard; public Wizard $wizard; public AccountForm $account; public ProfileForm $profile; public PreferencesForm $preferences; public function submit(): void { DB::transaction(function () { // $this->wizard->all() returns all form data keyed by step $data = $this->wizard->all(); // persist $data... }); } public function render(): View { return view('livewire.user-onboarding'); } protected function setupWizard(): array { return [ new Step( key: 'account', form: $this->account, view: 'livewire.wizard.steps.account', ), new Step( key: 'profile', form: $this->profile, view: 'livewire.wizard.steps.profile', skippable: true, ), new Step( key: 'preferences', form: $this->preferences, view: 'livewire.wizard.steps.preferences', ), ]; } }
Key points:
HasWizardprovidesnextStep(),previousStep(),skipStep(), andgoToStep(): wire these directly from your bladesetupWizard()is the only method you implement — return an array ofStepobjects- Mark optional steps with
skippable: true $this->wizard->all()returns all form data keyed by step key at submit time
Step 3: Create the Wizard View
<div> <x-ui.wizard variant="default"> <x-ui.wizard.steps color="orange"> <x-ui.wizard.step :active="$wizard->isActive('account')" :completed="$wizard->isCompleted('account')" :label="1" > <x-ui.heading>Account Information</x-ui.heading> <x-ui.text class="opacity-70">Create your account credentials.</x-ui.text> </x-ui.wizard.step> <x-ui.wizard.step :active="$wizard->isActive('profile')" :completed="$wizard->isCompleted('profile')" :label="2" > <x-ui.heading>Profile Information</x-ui.heading> <x-ui.text class="opacity-70">Tell us more about yourself.</x-ui.text> </x-ui.wizard.step> <x-ui.wizard.step :active="$wizard->isActive('preferences')" :completed="$wizard->isCompleted('preferences')" :label="3" > <x-ui.heading>Preferences & Review</x-ui.heading> <x-ui.text class="opacity-70">Customize your notification preferences.</x-ui.text> </x-ui.wizard.step> </x-ui.wizard.steps> <x-ui.wizard.body> <div class="p-6"> {{-- Render the current step's view dynamically --}} <x-dynamic-component :component="$wizard->currentStep()->view()" :statePath="$wizard->currentStep()->statePath()" /> <div class="flex items-center justify-between mt-8 pt-6"> @if (!$wizard->isFirst()) <x-ui.button wire:click="previousStep" size="sm" variant="soft"> Previous </x-ui.button> @endif <div class="flex gap-2 ml-auto"> @if ($wizard->isSkippable($wizard->currentKey()) && !$wizard->isLast()) <x-ui.button wire:click="skipStep" size="sm" variant="ghost"> Skip </x-ui.button> @endif @if (!$wizard->isLast()) <x-ui.button wire:click="nextStep" size="sm" variant="outline"> Next </x-ui.button> @else <x-ui.button wire:click="submit" size="sm" variant="outline" color="green"> Complete Setup </x-ui.button> @endif </div> </div> </div> </x-ui.wizard.body> </x-ui.wizard> </div>
Key points:
$wizard->isActive(),isCompleted(),isFirst(),isLast(),isSkippable()drive all the conditional UI$wizard->currentStep()->view()and->statePath()are passed tox-dynamic-componentso each step renders its own Blade partial with the correctwire:modelprefix- No
@ifchain per step — the dynamic component handles it cleanly
Step 4: Create Step Content Partials
Each step is a standalone Blade component that receives $statePath — use it as the wire:model prefix so bindings stay decoupled from the parent component's property names.
resources/views/livewire/wizard/steps/account.blade.php:
@props(['statePath']) <div class="space-y-6"> <div> <x-ui.heading size="lg">Account Information</x-ui.heading> <x-ui.text class="opacity-70 mt-2">Create your account credentials to get started.</x-ui.text> </div> <div class="space-y-4"> <x-ui.field required> <x-ui.label>Username</x-ui.label> <x-ui.input wire:model="{{ $statePath }}.username" type="text" placeholder="johndoe" autocomplete="username" /> <x-ui.error name="username" /> <x-ui.description>Choose a unique username that will identify you.</x-ui.description> </x-ui.field> <x-ui.field required> <x-ui.label>Email Address</x-ui.label> <x-ui.input wire:model="{{ $statePath }}.email" type="email" placeholder="john@example.com" autocomplete="email" /> <x-ui.error name="email" /> <x-ui.description>We'll send verification to this email.</x-ui.description> </x-ui.field> </div> </div>
resources/views/livewire/wizard/steps/profile.blade.php:
@props(['statePath']) <div class="space-y-6"> <div> <x-ui.heading size="lg">Profile Information</x-ui.heading> <x-ui.text class="opacity-70 mt-2">Tell us more about yourself to personalize your experience.</x-ui.text> </div> <div class="grid grid-cols-2 gap-4"> <x-ui.field required> <x-ui.label>First Name</x-ui.label> <x-ui.input wire:model="{{ $statePath }}.first_name" type="text" placeholder="John" /> <x-ui.error name="first_name" /> </x-ui.field> <x-ui.field required> <x-ui.label>Last Name</x-ui.label> <x-ui.input wire:model="{{ $statePath }}.last_name" type="text" placeholder="Doe" /> <x-ui.error name="last_name" /> </x-ui.field> </div> </div>
resources/views/livewire/wizard/steps/preferences.blade.php:
@props(['statePath']) <div class="space-y-6"> <div> <x-ui.heading size="lg">Preferences</x-ui.heading> <x-ui.text class="opacity-70 mt-2">Customize how you receive notifications.</x-ui.text> </div> <div class="space-y-3"> <x-ui.switch wire:model.live="{{ $statePath }}.email_notifications" label="Email Notifications" description="Receive notifications via email" /> <x-ui.switch wire:model.live="{{ $statePath }}.push_notifications" label="Push Notifications" description="Receive browser push notifications" /> <x-ui.switch wire:model.live="{{ $statePath }}.sms_notifications" label="SMS Notifications" description="Receive text message alerts" /> </div> </div>
Note: Always use
{{ $statePath }}.fieldNameas yourwire:modeltarget.statePath()resolves to the Livewire component property name that owns the form (e.g.account,profile), keeping your step partials fully reusable across different wizard instances.
How It Works
Step navigation: Clicking Next calls nextStep() which validates the current step's form. If validation passes, the step is marked complete and the wizard advances. Previous goes back without re-validating.
Skippable steps: When a step is marked skippable: true, a Skip button appears. Clicking it advances without triggering validation.
Dynamic rendering: x-dynamic-component renders whichever view the current step declares. No @if/$currentStep === 'x' chains needed — adding a new step is a single Step entry in setupWizard().
Final submission: $this->wizard->all() returns a keyed array of every form's data, ready to persist in a transaction.
Component Props
ui.wizard
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
string | 'default' |
Visual variant: 'default' or 'minimal' |
contained |
boolean | false |
Remove borders/spacing for custom containers |
ui.wizard.steps
| Prop | Type | Default | Description |
|---|---|---|---|
color |
string | null |
Accent color for active/completed states (any Tailwind color) |
variant |
string | inherited | Visual variant (inherited from parent wizard) |
contained |
boolean | inherited | Remove borders (inherited from parent wizard) |
ui.wizard.step
| Prop | Type | Default | Description |
|---|---|---|---|
active or data-active |
boolean | false |
Whether this is the current active step |
completed or data-active |
boolean | false |
Whether this step has been completed |
label |
mixed | 1 |
Label to display (number or custom content) |
icon |
slot | null |
Custom icon to replace label |
completedIcon |
string | 'check' |
Icon name to show when step is completed |
ui.wizard.body
| Prop | Type | Default | Description |
|---|---|---|---|
contained |
boolean | inherited | Remove border (inherited from parent wizard) |
Component API
Wizard
The core state machine. Instantiated automatically by HasWizard — you interact with it through $wizard in your component and views.
| Method | Returns | Description |
|---|---|---|
currentStep() |
Step |
The active step object |
currentKey() |
string |
The active step key |
steps() |
Collection |
All registered steps |
completed() |
array |
Keys of completed steps |
registry() |
array |
Persisted step metadata (key → skippable) (intrenals) |
isActive(string $key) |
bool |
Whether the given key is the current step |
isCompleted(string $key) |
bool |
Whether the given step has been completed |
isFirst() |
bool |
Whether the current step is the first |
isLast() |
bool |
Whether the current step is the last |
isSkippable(string $key) |
bool |
Whether the given step can be skipped |
next(bool $validate = true) |
void |
Advance to the next step, optionally validating first |
previous() |
void |
Go back one step, unmarking it as completed |
skip() |
void |
Advance without validation if the current step is skippable |
goTo(string $key) |
void |
Jump to a specific step (only back, or to completed steps) |
all() |
array |
All form data keyed by step key — use at submit time |
getState() |
array |
Serializable scalar state — used internally by the synthesizer |
Step
A value object describing a single wizard step. Created in setupWizard() and passed to the Wizard.
Constructor:
new Step( key: 'account', // string — unique identifier form: $this->account, // Form — the Livewire form instance for this step view: 'path.to.view', // string — Blade component path for the step content skippable: false, // bool — whether this step can be skipped (default: false) validate: true, // bool — whether nextStep() triggers validation (default: true) )
| Method | Returns | Description |
|---|---|---|
view() |
string |
The Blade component path — pass to x-dynamic-component :component |
statePath() |
string |
The Livewire property name owning the form — pass as :statePath |
validate() |
void |
Runs form validation if the form has rules. Safe to call unconditionally |
all() |
array |
All field values from the step's form |
HasWizard
A trait for your Livewire component. Bootstraps the wizard on every request and exposes navigation actions you wire directly from Blade.
Required: implement setupWizard(): array returning an array of Step objects.
| Method | Description |
|---|---|
setupWizard() |
Abstract. Return the array of Step objects defining your wizard |
nextStep() |
Advance to the next step with validation |
previousStep() |
Go back one step |
skipStep() |
Skip the current step if it is skippable |
goToStep(string $key) |
Jump to a specific step by key |
use HasWizard; public Wizard $wizard; // declare this — HasWizard bootstraps it automatically protected function setupWizard(): array { return [ new Step(key: 'account', form: $this->account, view: '...'), new Step(key: 'profile', form: $this->profile, view: '...', skippable: true), ]; }
WizardSynthesizer
Handles Livewire's dehydration/hydration cycle for the Wizard object. No configuration needed — register it once in a service provider and it works transparently.
// AppServiceProvider::boot() use App\Livewire\Synthesizers\WizardSynthesizer; use Livewire\Livewire; Livewire::propertySynthesizer(WizardSynthesizer::class);
After registration, any Livewire component property typed as
Wizardis automatically serialized between requests. The synthesizer persists only scalar state (current key, completed steps, registry) and letsHasWizardreattach the live form instances on every hydration — you never interact with it directly.