---
title: Wizard
description: A multi-step flow container — it holds the step, gates advancing, and announces the change.
visibility: guest
draft: false
---

# 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.

:::preview{title="Three Step Flow"}
<x-wirekit::wizard :steps="['Details', 'Payment', 'Review']">
    <x-wirekit::wizard.step :index="1">
        <x-wirekit::stack gap="sm">
            <x-wirekit::text weight="semibold">Your details</x-wirekit::text>
            <x-wirekit::input label="Full name" placeholder="Ada Lovelace" />
        </x-wirekit::stack>
    </x-wirekit::wizard.step>
    <x-wirekit::wizard.step :index="2">
        <x-wirekit::stack gap="sm">
            <x-wirekit::text weight="semibold">Payment</x-wirekit::text>
            <x-wirekit::input label="Card number" placeholder="4242 4242 4242 4242" />
        </x-wirekit::stack>
    </x-wirekit::wizard.step>
    <x-wirekit::wizard.step :index="3">
        <x-wirekit::stack gap="sm">
            <x-wirekit::text weight="semibold">Review</x-wirekit::text>
            <x-wirekit::text variant="muted">Everything looks right.</x-wirekit::text>
        </x-wirekit::stack>
    </x-wirekit::wizard.step>
</x-wirekit::wizard>
:::

## 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:

:::preview{title="Gated Step"}
<x-wirekit::wizard :steps="['Terms', 'Done']">
    <x-wirekit::wizard.step :index="1" :complete="false">
        <x-wirekit::stack gap="sm">
            <x-wirekit::text weight="semibold">Accept the terms</x-wirekit::text>
            <x-wirekit::text variant="muted" size="sm">Next is held back while this step is incomplete.</x-wirekit::text>
        </x-wirekit::stack>
    </x-wirekit::wizard.step>
    <x-wirekit::wizard.step :index="2">
        <x-wirekit::text>You would not get here.</x-wirekit::text>
    </x-wirekit::wizard.step>
</x-wirekit::wizard>
:::

```blade
{{-- 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.

::: warning 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:

```blade
{{-- 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](/components/stepper).

## Keyboard Interaction

| Key | Action |
| --- | --- |
| <kbd>Tab</kbd> | Moves through the fields of the current step, then to the controls |
| <kbd>Enter</kbd> / <kbd>Space</kbd> | 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.
