Skip to main content
Copy for LLM

Stepper

The <x-wirekit::stepper> 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

Horizontal Stepper
  1. Completed:
    Cart
  2. Shipping
  3. Payment
  4. Confirm

Steps before current show a checkmark. The current step is outlined with the accent color. Upcoming steps are muted.

With Descriptions

Stepper with Descriptions
  1. Completed:
    Cart
    Review items
  2. Completed:
    Shipping
    Address details
  3. Payment
    Card info
  4. Confirm
    Final review

Vertical Orientation

Vertical Stepper
  1. Completed:
    Account
    Create your account
  2. Profile
    Tell us about you
  3. Plan
    Choose 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.

Seven steps at phone width
  1. Completed:
    Goal
  2. Completed:
    Where it appears
  3. Audience
  4. Schedule and budget
  5. Brand safety
  6. Creative
  7. 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::wizard> 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:

Two steps done and clickable, the current one not
  1. Completed:
    Details
    Done — click to go back
  2. Completed:
    Documents
    Done — click to go back
  3. Review
    You are here
  4. Submit
    Not 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.

A step you did not give a destination stays presentational

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 via aria-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::wizard>: 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::wizard> keeps the same position for the forward direction: it adds Back and Next around the stepper and gates advancing.

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:

Green Accent Stepper
  1. Completed:
    Cart
  2. Completed:
    Shipping
  3. Payment
  4. Confirm
Purple Accent Stepper
  1. Completed:
    Cart
  2. Shipping
  3. Payment
  4. Confirm

Completed Step at End

All Steps Completed
  1. Completed:
    Cart
  2. Completed:
    Shipping
  3. Completed:
    Payment
  4. Completed:
    Confirm

Vertical with Custom Color

Vertical Stepper — Blue Accent
  1. Completed:
    Order Placed
    April 1, 2026
  2. Completed:
    Processing
    April 2, 2026
  3. Shipped
    Estimated April 5
  4. Delivered
    Pending

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/wirekit.php under components.stepper. Override them globally:

'components' => [
    'stepper' => ['orientation' => 'vertical'], // default to vertical
],

Further Reading

Updated in WireKit v2.66.0 (2026-10-13)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store