Skip to main content
WireKit
Copy for LLM

Wizard

<x-wirekit::stepper> draws where you are and does not know it. It is an <ol> driven by a current prop, with no state and no controls — correct for an indicator, and it left every application to rebuild the same three things around it: which step is showing, whether you may leave it, and how the change reaches somebody who cannot see it.

<x-wirekit::wizard> is that container. It uses the stepper rather than replacing it.

Three Step Flow
  1. Details
  2. Payment
  3. Review

Your details

Payment

Review

Everything looks right.

Gating a step

Pass complete and the flow will not leave that step until it is true. Drive it from whatever already knows — a Livewire property, a validated form object:

Gated Step
  1. Terms
  2. Done

Accept the terms

Next is held back while this step is incomplete.

You would not get here.

{{-- 1. `complete` is read from the DOM each time Next is pressed, not cached — so a
       server-side re-render lands without anything having to notify Alpine. --}}
<x-wirekit::wizard.step :index="1" :complete="$form->detailsAreValid">
    …
</x-wirekit::wizard.step>

Going back is never gated. The condition guards leaving a step forward with it unfinished; refusing to return would strand somebody on the step they cannot complete.

The step change is announced

A step change replaces a panel with no page load behind it: focus has not moved, no landmark changed, and the new panel simply exists. Nothing tells a screen reader anything happened.

So the wizard carries a polite live region and writes a sentence into it — "Step 2 of 3: Payment" — built from a translatable template rather than assembled from fragments. It is present from the first render and empty until a step changes, so nothing is read on load.

A step is hidden, not destroyed The panel uses x-show plus hidden, and the pair is deliberate. x-if would destroy the panel, so every field on a step you stepped back from would silently lose what was typed in it, along with any wire:model binding — which is the difference between a multi-step form and a tab set. Do not swap it.

hidden alongside x-show keeps the inactive panel out of the accessibility tree and out of the tab order. Without it a four-step flow is read as one long page.

Your own controls

Pass a controls slot to replace the default Back and Next — a submit button on the last step, a Cancel, whatever the flow needs. next(), prev(), canAdvance, isFirst and isLast are all available in that scope:

{{-- 2. The default controls disappear as soon as you supply your own. --}}
<x-wirekit::wizard :steps="$steps">
    …
    <x-slot:controls>
        <x-wirekit::row justify="end" gap="sm">
            <x-wirekit::button surface="ghost" x-on:click="prev()" x-bind:hidden="isFirst ? true : null">Back</x-wirekit::button>
            <x-wirekit::button x-on:click="next()" x-bind:hidden="isLast ? true : null">Continue</x-wirekit::button>
            <x-wirekit::button wire:click="submit" x-bind:hidden="isLast ? null : true">Place order</x-wirekit::button>
        </x-wirekit::row>
    </x-slot:controls>
</x-wirekit::wizard>

Props

<x-wirekit::wizard>

Prop Type Default Description
steps array [] Step names, in order. They drive the indicator and the announcement.
current int 1 Which step shows first, 1-based. Clamped to the number of steps.
indicator bool true Draw the stepper above the panel. Turn it off when the page already shows progress its own way — two indicators for one flow is worse than one.
orientation string horizontal Passed through to the stepper.
scope string|null null Scoped personalization key

<x-wirekit::wizard.step>

Prop Type Default Description
index int required Position in the flow, 1-based. The container finds a step by this number rather than by DOM order, so a conditionally rendered step cannot shift the others.
complete bool|null null Whether the flow may leave this step. Unset means yes.
scope string|null null Scoped personalization key

Sub-Components

Component Purpose
wizard.step One panel of the flow, shown when its index is current

Accessibility

  • The step change is announced through a role="status" / aria-live="polite" region that exists from the first render.
  • An inactive panel carries hidden, so it is out of both the accessibility tree and the tab order.
  • The default controls are ordinary buttons — the keyboard model is the browser's, and there is nothing bespoke to get wrong.
  • Next carries aria-disabled rather than disabled while a step is incomplete, so it stays focusable and the reason it will not advance can be announced.
  • The indicator stays presentational. It reflects the position and owns no navigation, which is the stepper's own documented position.

Keyboard Interaction

Key Action
Tab Moves through the fields of the current step, then to the controls
Enter / Space Activates the focused control

There is no wizard-specific key model, and that is deliberate: the controls are buttons, so the browser's own behavior is the whole contract.

Pitfalls

  • Number the steps. A wizard.step without index can never be shown and never gates anything; it says so in the console while app.debug is on.
  • Do not make the indicator clickable. It shows where the reader is; it is not a nav. A jump-back in a flow with validation undermines exactly what the gating is for.
  • One indicator per flow. If the page already shows progress, pass indicator="false" rather than letting two of them disagree.