Stepper
The <x-wirekit:: component shows progress through a multi-step flow. Each step is automatically styled based on its position relative to the current step: completed (filled), current (outlined), or upcoming (muted).
Usage
-
Completed:Cart
-
Shipping
-
Payment
-
Confirm
Steps before current show a checkmark. The current step is outlined with the accent color. Upcoming steps are muted.
With Descriptions
-
Completed:CartReview items
-
Completed:ShippingAddress details
-
PaymentCard info
-
ConfirmFinal review
Vertical Orientation
-
Completed:AccountCreate your account
-
ProfileTell us about you
-
PlanChoose a plan
Many steps in a narrow column
Every step of a horizontal stepper gets an equal column. With seven steps on a phone a column is about 50px, narrower than most words. Below 5rem per column the stepper therefore draws a compact form on its own: the circles stay in their row, the labels are hidden visually, and one line under the row names the current step. The column decides, not the screen, so a stepper in a narrow dialog on a wide screen takes the same form.
-
Completed:Goal
-
Completed:Where it appears
-
Audience
-
Schedule and budget
-
Brand safety
-
Creative
-
Review
Wider than that, a word that fits its column stays whole, and a longer one breaks at a syllable with a hyphen where the page's lang says how. Keeping the shorter word whole is progressive enhancement too: a browser with hyphenate-limit-chars, and Safari through its own prefixed limits, leaves it alone, and a browser with neither may hyphenate it at the end of a line. The compact form is progressive enhancement: it relies on contain-intrinsic-inline-size, which Safari has from version 17, and a browser without it shows a label in every column.
Following a flow that changes step in the browser
current draws the stepper once, on the server. When the step changes in the browser, name the Alpine property that holds it in follow, and the stepper moves with it: the current step, the completed ones and their check marks, and the line of the compact form. current still draws the first paint, so the page is right before Alpine starts.
<div x-data="checkout">
<x-wirekit::stepper :steps="['Details', 'Payment', 'Review']" :current="1" follow="step" />
<x-wirekit::button x-on:click="next()">Next</x-wirekit::button>
</div>
Alpine.data('checkout', () => ({
step: 1,
next() {
this.step = Math.min(this.step + 1, 3);
},
}));
<x-wirekit:: does this for you. follow takes a property name and nothing else; a step in a following stepper never becomes a link or a button, because which steps are completed changes after the page was drawn.
Letting the reader step back
A stepper looks like the way back, and a finished step that does not answer a click costs the reader a click and a guess about the state of the page. Give the step a destination and it becomes one:
{{-- 1. `href` renders a real <a>; use `wire:click` instead for a Livewire action. --}}
{{-- 2. Only steps BEFORE `current` become operable — the rest stay presentational. --}}
<x-wirekit::stepper :current="3" :steps="[
['label' => 'Details', 'href' => route('wizard.details')],
['label' => 'Documents', 'href' => route('wizard.documents')],
['label' => 'Review'],
]" />
The first two carry a pointer and a focus ring; the third does not, and neither would a fourth. Hover the finished steps to see the difference:
-
Completed:DetailsDone — click to go back
-
Completed:DocumentsDone — click to go back
-
ReviewYou are here
-
SubmitNot offered, even with an href
Only http, https, mailto, tel and relative addresses become links. An href with any
other scheme, such as javascript:, is dropped, and so is an empty one. The step then renders as
if it had none: a button when it carries a wire:click, plain text when it does not.
A wire:click is an expression that Livewire evaluates when the reader clicks the step. Write it
yourself and never fill it from data. It is read from the wire:click key and from no other, and
an empty one renders no button.
Being completed is not by itself a reason to be clickable. Your flow decides by supplying a target, so a stepper built from plain strings renders exactly as it always has — no tag, no class, no attribute added.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
steps |
array |
[] |
Array of step labels (strings) or arrays taking label, description, and — for a completed step — href or wire:click |
current |
int |
1 |
1-based index of the current step |
orientation |
string |
'horizontal' |
'horizontal' or 'vertical' |
follow |
string|null |
null |
The name of an Alpine property holding the current step, 1-based, for a flow that changes step in the browser. See Following a flow that changes step in the browser |
scope |
string|null |
null |
Scoped personalization key |
Accessibility
- Outer
<ol aria-label="Progress">— semantic ordered list - Current step marked with
aria-current="step"on its<li> - The current step's label is set in the heading weight, so the current step is marked by more than the color of its circle. The weight follows
aria-current, also on a stepper that follows a flow in the browser - The current step's circle and the circles after it are outlines that differ in color. Under forced colors, which paints both in one system color, the current step's circle is framed in the system's selection color
- Completed steps include a visually hidden "Completed:" label for screen readers — the word runs through Laravel's
__(), so it follows your app locale rather than staying English (see Localization) - Step numbers and icons are
aria-hidden="true"(state conveyed viaaria-current) - Connector lines are
aria-hidden="true"(decorative) - In the compact form the labels are hidden only visually, so every step is still read with its label; the line under the row repeats the current step for sighted readers and is
aria-hidden="true"
Keyboard Interaction
By default the stepper is a presentational progress indicator — a semantic <ol> of steps with no focusable controls of its own. The current position is conveyed with aria-current, and the numbers, icons and connector lines are aria-hidden. Nothing to tab to, nothing to press.
Give a step an href or a wire:click and it becomes a real link or button — but only once it is completed (see "Letting the reader step back" above). A completed step is then an ordinary tab stop with a focus ring, and its accessible name is built from everything the element wraps — which is more than the label. The link covers the circle and the label block, so the name is the visually hidden "Completed:" prefix, then the label, then the description if the step has one. The first step in the preview under "Letting the reader step back" is announced as "Completed: Details Done — click to go back", not as "Details".
That is worth knowing before you write a description on a step you intend to make operable: it is part of that step's name, not a caption sitting beside it. Write it so it reads as one phrase with the label, or leave it off.
If your flow needs the user to move forward between steps, use <x-wirekit::: it holds the step, gates advancing on a per-step condition, announces the change to a screen reader, and renders this stepper inside itself. The stepper reflects state either way — it does not own the navigation, and the wizard does not change that.
Driving current from your own controls stays perfectly valid and is the right answer when the flow's state already lives somewhere else.
Pitfalls
- A step becomes clickable only when you give it somewhere to go, and only once it is completed. The stepper shows where the reader is; it is not a nav, and it will not invent one. An upcoming step never becomes operable even with an
href, because a jump forward is a permission only your flow can grant — a clickable skip in a checkout walks around the validation the steps exist for.<x-wirekit::keeps the same position for the forward direction: it adds Back and Next around the stepper and gates advancing.wizard>
Design Tokens
| Token | Used for |
|---|---|
--font-wk-heading-weight |
Step number font weight, and the current step's label |
--text-wk-xs / --text-wk-sm |
Step number / label font size |
--color-wk-text |
Step label |
--color-wk-text-muted |
Upcoming circle number + step description |
--color-wk-accent |
Completed circle background + current and completed circle border |
--color-wk-accent-fg |
Completed circle check mark |
--color-wk-accent-text |
Current circle number |
--color-wk-bg |
Current and upcoming circle background |
--color-wk-border |
Pending circle border + connector line |
--border-wk-width |
Circle + connector border width |
--padding-wk-x-sm / --padding-wk-y-xs / --padding-wk-y-md |
Step item padding |
--transition-wk-duration |
State transition |
Customization
Accent Color
The step indicator circles use --color-wk-accent for the current and completed states. Override it to match your brand:
-
Completed:Cart
-
Completed:Shipping
-
Payment
-
Confirm
-
Completed:Cart
-
Shipping
-
Payment
-
Confirm
Completed Step at End
-
Completed:Cart
-
Completed:Shipping
-
Completed:Payment
-
Completed:Confirm
Vertical with Custom Color
-
Completed:Order PlacedApril 1, 2026
-
Completed:ProcessingApril 2, 2026
-
ShippedEstimated April 5
-
DeliveredPending
Token Map by Element
| Element | Token |
|---|---|
| Circle completed bg | --color-wk-accent |
| Circle completed text | --color-wk-accent-fg |
| Circle current border | --color-wk-accent |
| Circle current text | --color-wk-accent-text |
| Circle upcoming border | --color-wk-border |
| Circle upcoming text | --color-wk-text-muted |
| Connector line | --color-wk-border |
| Label text | --color-wk-text |
| Description text | --color-wk-text-muted |
| Circle size | --wk-stepper-circle, var(--size-wk-sm) (2rem) unless set; the connector line follows it |
| Font weight (circle) | --font-wk-heading-weight |
| Font weight (current step's label) | --font-wk-heading-weight |
| Transition | --transition-wk-duration |
The circle's size is the custom property --wk-stepper-circle, and the connector line computes
its position from the same property, so it stays on the circle's center at any size. Set it on
one stepper with style:
<x-wirekit::stepper style="--wk-stepper-circle: var(--size-wk-md)" :steps="['A', 'B', 'C']" :current="2" />
To change it for every stepper in a scope, add it to the list through scoped personalization:
// In a service provider's boot() method. A closure receives the shipped classes, so the
// component keeps them; a plain string would replace them outright.
WireKit::scope('my-scope', [
'stepper' => ['classes' => [
// Larger circles; the connector follows, because it reads the same property.
'list' => fn (string $vendor): string => $vendor.' [--wk-stepper-circle:var(--size-wk-md)]',
]],
]);
<x-wirekit::stepper scope="my-scope" :steps="['A', 'B', 'C']" :current="2" />
Resizing the circle through its own classes ('circle' => …) moves the circle and leaves the
line where a 2rem circle would have its center.
Config Defaults
The defaults live in config/ under components.. Override them globally:
'components' => [
'stepper' => ['orientation' => 'vertical'], // default to vertical
],
Further Reading
- MDN:
aria-current— used with value"step"to mark the active step - MDN:
<ol>element — semantic ordered list - WAI-ARIA: step indicator patterns