Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Livewire Steps Laravel Package

satoved/laravel-livewire-steps

Lightweight Livewire component for building wizards (multi-step forms). Each step is a Livewire 3 form object, while a single WizardComponent handles navigation. Extend WizardComponent, list your steps as public properties, and render the current step with minimal code.

View on GitHub
Deep Wiki
Context7

Getting Started

Install it and move on — there is nothing else to configure:

composer require satoved/laravel-livewire-steps

No config file to publish, no migrations, no service provider, no assets. The package is essentially two base classes, so "getting started" means reading them, not following a setup guide. Do this before writing your first wizard:

  1. src/Livewire/WizardComponent.php — the whole navigation state machine. Establish precisely how the current step is tracked (index? property name?), what nextStep() does on validation failure (throws? returns? silent?), whether previousStep() exists, and how renderStep() resolves the active step. This is the only real documentation for anything the README omits.
  2. src/Livewire/Forms/StepForm.php — the base step. Check what render() must return (string vs View), and what it inherits from Livewire's Form (notably validate(), reset(), and all()).

Then look at satoved/laravel-livewire-steps-demo, the companion repo with a two-step email subscription flow. It's the fastest way to see the Blade side end to end, which the README compresses into heredocs.

The minimal shape is one Livewire component with one public property per step, declared in flow order:

use Satoved\LivewireSteps\Livewire\WizardComponent;

class SignupWizard extends WizardComponent
{
    public NameStep $nameStep;
    public EmailStep $emailStep;

    public function render()
    {
        return view('livewire.signup-wizard');
    }
}
use Satoved\LivewireSteps\Livewire\Forms\StepForm;

class EmailStep extends StepForm
{
    #[Validate(['required', 'email'])]
    public string $email = '';

    public function render()
    {
        return view('livewire.steps.email');
    }
}

And the wizard template:

<form wire:submit="nextStep">
    {{ $this->renderStep() }}
    <button type="submit">Next</button>
</form>

Your first use case should be a 2–3 step, linear, server-validated flow: onboarding intake, newsletter signup, a contact form with a confirmation step. Do not debut this on checkout. The package gives you step sequencing and per-step validation — nothing else. Payments, stock reservation, and idempotency are all yours to design, and debugging those while also learning an undocumented step machine is a bad trade.

Implementation Patterns

Step order is declaration order — treat it as the source of truth. The flow is the order of the public step properties on the wizard class. Keep that block contiguous, comment it, and make it obvious in review that reordering it reorders the wizard. Anything that reads the flow — progress bars, step labels, analytics, breadcrumbs — should derive from that block (a computed property that reflects over the public properties) rather than a second hardcoded list that will drift.

Render from Blade views, never inline strings. The README returns raw HTML strings from render(); that's demo code written for brevity. In a real app it means your step markup can't use your Blade components, can't be linted or reviewed as a template, and forces you to hand-write every @error and wire:key decision inside a PHP class. Standardise on return view('livewire.steps.email') for the whole team on day one — it's the highest-leverage convention with this package.

Qualify every binding with the step property name. Step markup renders inside the wizard's scope, so bindings address the step through the wizard property: wire:model="emailStep.email". Validation errors are keyed identically, so the matching template needs @error('emailStep.email'). Know this early, because it means renaming a step property is a breaking change across your app, not a local rename.

Keep per-step rules on the step, cross-step rules on the wizard. #[Validate] attributes on step properties give you per-step validation for free — that's where this package genuinely shines. Anything spanning steps (password/confirmation, end-after-start, "email not already registered") has no natural home in a step form object and must be enforced explicitly on the wizard: typically in the completion path or in an overridden transition method.

Assume earlier steps are not re-validated. Navigate forward past a valid step, go back, edit that field to something invalid, then jump forward and finish — the classic bug is that only the final step's rules get evaluated. Add an explicit "revalidate every step" pass before your completion handler runs:

protected function revalidateAll(): void
{
    foreach ($this->stepProperties() as $property) {
        $this->{$property}->validate();
    }
}

It's a few lines and it closes the most common correctness hole in wizard implementations.

Wrap the package exactly once. There is no config surface, so all customisation is subclassing. Put one thin base class in app/Livewire/Wizards/ and have every wizard extend it:

class BaseWizard extends WizardComponent
{
    // progress reporting, persistence hooks, analytics events, step guards
}

That single file becomes your seam for everything: adding progress state, intercepting transitions, emitting events, and — if the package stalls or a Livewire release breaks it — swapping to a fork or a hand-rolled base without touching each wizard.

Navigation chrome is entirely your job. The package drives step state only: no progress bar, no back button, no step labels, no "Step 2 of 4". Build these against your own derived step list and expose them as computed properties so Blade never reaches into package internals. Reflecting on protected state from a view is the thing most likely to break in a minor release.

Skip support exists but is undocumented. v1.1.0 added a skip-step capability that the README never mentions. Read WizardComponent for the real method name and semantics — does it advance the index, bypass validation, emit anything? If you depend on it, wrap it in your own named method so call sites don't care what the package calls it.

Persistence is application work. Defaults rely on Livewire component state, so a hard refresh restarts the wizard at step one. For long or high-stakes flows, persist the current step plus the validated payload on each transition and rehydrate in mount(). Plan it at design time; retrofitting after steps have side effects is painful.

Test transitions, not the package. These are ordinary Livewire components, so standard tooling applies:

Livewire::test(SignupWizard::class)
    ->set('emailStep.email', 'not-an-email')
    ->call('nextStep')
    ->assertHasErrors('emailStep.email');

Livewire::test(SignupWizard::class)
    ->set('nameStep.name', 'Ada')
    ->call('nextStep')
    ->assertSet('nameStep.name', 'Ada'); // data survives the transition

Lock these invariants in CI: an invalid step blocks advancement, a valid step advances, going back preserves data, and the finish handler fires exactly once. Those are what break on upgrade — and this package's release history is largely compatibility bumps (Livewire 4, Laravel 13), so upgrades will happen.

Gotchas and Tips

A silent no-op is almost always validation. When nextStep "does nothing," it's failing validation with no visible error in your step view. Add an @error block or a wire:loading indicator on the submit button before you go digging into package internals — this saves an hour the first time.

Indices are positional, so never persist them. Inserting a step mid-list silently redefines what "step 2" means. If you store an integer index in a session or a database row, inserting a step corrupts every in-flight wizard. Persist the step's name (property name or a stable slug you associate with it) and resolve it back to a position at hydration time.

Validate against the correct error bag keys. Because errors are namespaced stepProperty.field, a naive @error('email') renders nothing and looks like validation isn't running. If you use $errors->any() for a global banner, remember it will be true for any step's errors, including ones from a step the user can't currently see.

Keep a wire:key on the step container. Step content swaps in place; without a stable key you can get stale DOM state (focused inputs, third-party JS widgets, file inputs) when moving between steps. wire:key="step-{{ $this->currentStepName }}" on the wrapper fixes it. This matters more with Alpine components inside steps.

Hydration limits apply to form objects. Steps are Livewire form objects, so only public, serialisable properties survive round-trips. Don't stash models, closures, or uploads as arbitrary properties without understanding Livewire's rules — and remember wire:model on a file input needs Livewire's WithFileUploads handling, which is on the wizard, not the step.

Step classes aren't components. A StepForm has a render() but no lifecycle hooks, no mount(), no #[On] listeners. If a step needs to react to something (recompute a total, load options), do it in the wizard and mutate the step's public property — or add a small helper method on the step and call it from the wizard.

Completion is not built in. There's no finish() or onComplete() hook. When the last step validates, you decide what happens — dispatch an event, redirect, persist a model. Detect "last step" by comparing the current position to the count of step properties, and make firing exactly once an explicit concern (guard with a flag or a terminal step you advance past), since a double-submit otherwise creates duplicate records.

Dependency risk is real. Six stars, one maintainer, zero dependents. That's fine for a 200-line package you can read in ten minutes, but it means you must treat the source as your documentation and your BaseWizard as your insurance policy. If the package dies, your replacement is half a day's work — provided you wrapped it.

Debugging tips. dump($this->all()) or Livewire's devtools will show the step property keys and current position; ->assertSet() in a test is usually faster than clicking through the browser. If a binding appears to do nothing, check the full wizardProperty.field path in the template — a missing prefix is the single most common copy-paste error when duplicating step markup.

Extension points in order of safety: (1) subclass WizardComponent, (2) override renderStep() to decorate the returned view, (3) add computed properties for progress/labels, (4) override nextStep() — last resort, and always parent::nextStep() unless you deliberately want to bypass validation.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style