Skip to content
Brok UI

Loading…

No results

Timeline

Open source

Sequenced event displays — a free-form timeline and a stepper that covers both a numbered install/checkout sequence and a plain status stepper.

Version
v1.5.2
Stability
stable
License
MIT
Related
Timeline
Progress

Stepper — A step-by-step layout with connector lines and complete/active/upcoming states, vertical or horizontal, numbered or a plain dot marker, for install guides, onboarding, checkout progress, fulfilment tracking and event timelines.

Preview

  1. Completed
    Install the package

    Add the dependency to your project.

  2. 2
    Configure the registry

    Point the CLI at your registry endpoint.

  3. 3
    Add your first component

    Run ui:add to copy editable Blade in.

  1. Completed
    Cart
  2. 2
    Shipping
  3. 3
    Payment
  1. Completed
    Order placed

    Your order has been received and confirmed.

  2. Preparing shipment

    Your items are being packed and labeled.

  3. Out for delivery

    A courier will bring your package to your door.

Orientation
Align
Tone
previews.components.stepper.default.blade.php Blade
<div class="grid w-full gap-10 lg:grid-cols-2">
    <x-ui.stepper
        :steps="[
            ['title' => 'Install the package', 'description' => 'Add the dependency to your project.', 'status' => 'completed'],
            ['title' => 'Configure the registry', 'description' => 'Point the CLI at your registry endpoint.', 'status' => 'active'],
            ['title' => 'Add your first component', 'description' => 'Run ui:add to copy editable Blade in.', 'status' => 'default'],
        ]"
    />

    <x-ui.stepper orientation="horizontal" class="self-start">
        <x-ui.stepper.item step="1" title="Cart" status="completed" />
        <x-ui.stepper.item step="2" title="Shipping" status="active" />
        <x-ui.stepper.item step="3" title="Payment" status="default" :last="true" />
    </x-ui.stepper>

    {{-- numbered=false: a plain dot marker for a status timeline (the look
         formerly shipped as the separate timeline-steps component). --}}
    <x-ui.stepper :numbered="false" class="max-w-md">
        <x-ui.stepper.item status="completed" title="Order placed" description="Your order has been received and confirmed." />
        <x-ui.stepper.item status="active" title="Preparing shipment" description="Your items are being packed and labeled." />
        <x-ui.stepper.item status="default" title="Out for delivery" description="A courier will bring your package to your door." :last="true" />
    </x-ui.stepper>
</div>

Orientation options

Vertical Current
Horizontal Current

Align options

Start Current
Center Current

Tone options

Primary Current
Completed Current
Active Current
Connector Current
Neutral Current

Installation

terminal
php artisan ui:add stepper

Registry contract

php artisan ui:add stepper writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.

  • blade resources/views/components/ui/stepper.blade.php
  • blade resources/views/components/ui/stepper/item.blade.php
Registry dependencies
None — installs on its own.
Packages
composer: jml/brok:^0.2

Use with AI

A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.

stepper.md
# Brok UI: Stepper (`stepper`)

A step-by-step layout with connector lines and complete/active/upcoming states, vertical or horizontal, numbered or a plain dot marker, for install guides, onboarding, checkout progress, fulfilment tracking and event timelines.

Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.

## Install

```bash
php artisan ui:add stepper
```

## Usage

```blade
<div class="grid w-full gap-10 lg:grid-cols-2">
    <x-ui.stepper
        :steps="[
            ['title' => 'Install the package', 'description' => 'Add the dependency to your project.', 'status' => 'completed'],
            ['title' => 'Configure the registry', 'description' => 'Point the CLI at your registry endpoint.', 'status' => 'active'],
            ['title' => 'Add your first component', 'description' => 'Run ui:add to copy editable Blade in.', 'status' => 'default'],
        ]"
    />

    <x-ui.stepper orientation="horizontal" class="self-start">
        <x-ui.stepper.item step="1" title="Cart" status="completed" />
        <x-ui.stepper.item step="2" title="Shipping" status="active" />
        <x-ui.stepper.item step="3" title="Payment" status="default" :last="true" />
    </x-ui.stepper>

    {{-- numbered=false: a plain dot marker for a status timeline (the look
         formerly shipped as the separate timeline-steps component). --}}
    <x-ui.stepper :numbered="false" class="max-w-md">
        <x-ui.stepper.item status="completed" title="Order placed" description="Your order has been received and confirmed." />
        <x-ui.stepper.item status="active" title="Preparing shipment" description="Your items are being packed and labeled." />
        <x-ui.stepper.item status="default" title="Out for delivery" description="A courier will bring your package to your door." :last="true" />
    </x-ui.stepper>
</div>
```

## Props

- `orientation` (vertical|horizontal, default `vertical`) — Rail direction. Vertical stacks marker over connector; horizontal lays the steps across the row.
- `align` (start|center, default `start`) — Horizontal only. `center` puts each marker in the middle of its own column with the label beneath it and draws the rail marker-centre to marker-centre — the checkout/fulfilment shape.
- `steps` (array, default `[]`) — Step entries: ['step','title','description','status','icon']. `icon` takes an Htmlable (e.g. new HtmlString('<svg …>')) shown in the marker for non-completed steps; a completed step always shows the check. Omit `steps` to compose <x-ui.stepper.item> children by hand.
- `numbered` (bool, default `true`) — false hides the step number (and the icon fallback slot) and leaves a plain dot marker — the look formerly shipped as the separate timeline-steps component, now this component's own event-timeline mode. A completed step always shows its check regardless.
- `bind` (string|null, default `null`) — An Alpine expression for the current 1-based step (`step`). Every item then follows it live: marker, connector, check and aria-current update as the value changes, so a wizard needs no second rail.
- `tone` (primary|neutral, default `primary`) — Colour of the current and completed markers and of the done connectors: primary uses the brand primary; neutral uses the foreground colour, so a product that keeps primary for its one main action is not diluted by progress marks. Upcoming steps stay muted in both.
- `step` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
- `title` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
- `description` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
- `status` (string, default `default`) — Declared by @props in the registry Blade source.
- `icon` (mixed|null, default `null`) — Declared by @props in the registry Blade source.
- `last` (bool, default `false`) — Declared by @props in the registry Blade source.

## Use when

- Use to show real progress, ordered stages, or process status when visibility helps completion.
- Showing where someone is in a fixed, ordered sequence — an install guide, an onboarding flow, a checkout.
- Reporting fulfilment progress across named stages: orientation="horizontal" align="center" with a per-step icon.

## Avoid when

- Do not add progress UI to tiny tasks or to flows whose steps are not meaningfully sequential.
- Use for ordered stages with clear progression; start complex flows as focused steps and merge only when research supports it.
- The entries are dated events rather than a sequence to finish — use <x-ui.timeline>.
- There is one continuous quantity to report — use <x-ui.progress>.

## Anti-patterns

- Decorative or inaccurate progress
- Steppers for nonsequential exploration

## Rules

- Use the `<brok:stepper>` tag (or `<x-ui.stepper>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.

## Links

- Docs: https://brokui.dev/docs/components/stepper
- Registry JSON (files, props, contract): https://brokui.dev/r/open/stepper.json

Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.

Examples

horizontal.blade.php Blade
@php
    $icon = fn (string $path) => new \Illuminate\Support\HtmlString(
        '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="'.$path.'" /></svg>'
    );
@endphp

<div class="flex w-full flex-col gap-10">
    <x-ui.stepper
        orientation="horizontal"
        align="center"
        :steps="[
            ['title' => __('Cart'), 'status' => 'completed'],
            ['title' => __('Shipping'), 'status' => 'completed'],
            ['title' => __('Payment'), 'status' => 'active'],
            ['title' => __('Review'), 'status' => 'default'],
        ]"
    />

    <x-ui.stepper
        orientation="horizontal"
        align="center"
        :steps="[
            ['title' => __('Ordered'), 'status' => 'completed', 'icon' => $icon('M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4zM3 6h18M16 10a4 4 0 0 1-8 0')],
            ['title' => __('Packed'), 'status' => 'completed', 'icon' => $icon('M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z')],
            ['title' => __('Shipped'), 'status' => 'active', 'icon' => $icon('M10 17h4V5H2v12h3m15 0h2v-3.34a4 4 0 0 0-1.17-2.83L19 9h-5v8h1')],
            ['title' => __('Delivered'), 'status' => 'default', 'icon' => $icon('M20 6 9 17l-5-5')],
        ]"
    />
</div>
tones.blade.php Blade
{{-- tone="primary" (the default) draws the current and completed marks in the
     brand primary; tone="neutral" uses the foreground colour, for a product
     that keeps primary for its one main action. --}}
<div class="flex w-full flex-col gap-10">
    <x-ui.stepper
        orientation="horizontal"
        align="center"
        :steps="[
            ['title' => __('Account'), 'status' => 'completed'],
            ['title' => __('Workspace'), 'status' => 'active'],
            ['title' => __('Invite team'), 'status' => 'default'],
        ]"
    />

    <x-ui.stepper
        tone="neutral"
        orientation="horizontal"
        align="center"
        :steps="[
            ['title' => __('Account'), 'status' => 'completed'],
            ['title' => __('Workspace'), 'status' => 'active'],
            ['title' => __('Invite team'), 'status' => 'default'],
        ]"
    />
</div>

API

manifest knowledge + registry-derived coverage

Props

Props accepted by this component: name, type, default value and description.
Prop Type Default Description
orientation vertical | horizontal vertical Rail direction. Vertical stacks marker over connector; horizontal lays the steps across the row.
align start | center start Horizontal only. `center` puts each marker in the middle of its own column with the label beneath it and draws the rail marker-centre to marker-centre — the checkout/fulfilment shape.
steps array [] Step entries: ['step','title','description','status','icon']. `icon` takes an Htmlable (e.g. new HtmlString('<svg …>')) shown in the marker for non-completed steps; a completed step always shows the check. Omit `steps` to compose <x-ui.stepper.item> children by hand.
numbered bool true false hides the step number (and the icon fallback slot) and leaves a plain dot marker — the look formerly shipped as the separate timeline-steps component, now this component's own event-timeline mode. A completed step always shows its check regardless.
bind string | null null An Alpine expression for the current 1-based step (`step`). Every item then follows it live: marker, connector, check and aria-current update as the value changes, so a wizard needs no second rail.
tone primary | neutral primary Colour of the current and completed markers and of the done connectors: primary uses the brand primary; neutral uses the foreground colour, so a product that keeps primary for its one main action is not diluted by progress marks. Upcoming steps stay muted in both.
step mixed | null null Declared by @props in the registry Blade source.
title mixed | null null Declared by @props in the registry Blade source.
description mixed | null null Declared by @props in the registry Blade source.
status string default Declared by @props in the registry Blade source.
icon mixed | null null Declared by @props in the registry Blade source.
last bool false Declared by @props in the registry Blade source.

Slots

  • default — Primary Blade slot rendered by the component.
  • x-ui.stepper.item — Installed subcomponent from the registry item.

Data slots

Stable hooks for CSS overrides and browser tests.

stepper stepper-connector stepper-content stepper-description stepper-item stepper-marker stepper-title

Behavior

  • With `bind`, the server-rendered statuses are the first paint and the Alpine expression drives them afterwards through object-form class bindings, so exactly one state's classes stay on each marker.
  • Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.

Guidance

Progress and sequence

Communicate genuine progress or ordered stages.

Use when

  • Use to show real progress, ordered stages, or process status when visibility helps completion.
  • Showing where someone is in a fixed, ordered sequence — an install guide, an onboarding flow, a checkout.
  • Reporting fulfilment progress across named stages: orientation="horizontal" align="center" with a per-step icon.

Avoid when

  • Do not add progress UI to tiny tasks or to flows whose steps are not meaningfully sequential.
  • Use for ordered stages with clear progression; start complex flows as focused steps and merge only when research supports it.
  • The entries are dated events rather than a sequence to finish — use <x-ui.timeline>.
  • There is one continuous quantity to report — use <x-ui.progress>.

Use instead

  • Simple completion text for tiny tasks

Anti-patterns

  • Decorative or inaccurate progress
  • Steppers for nonsequential exploration
Anatomy
stepper stepper-item stepper-marker stepper-connector stepper-content stepper-title stepper-description
Theming hooks
stepper stepper-connector stepper-content stepper-description stepper-item stepper-marker stepper-title

Accessibility

WCAG 2.2 AA Keyboard focus-visible RTL-ready Localized labels Dark mode
Keyboard
managed
Focus
none
  • Renders an ordered list, so assistive technology announces the step count and position; the active step carries aria-current="step", also when it follows a bound value.
  • Connectors and the complete check are decorative (aria-hidden); a completed step also says "Completed" in visually hidden text, so the state never depends on the icon or colour alone.
  • The active-step pulse runs only under prefers-reduced-motion: no-preference.
  • Semantic HTML and a stable data-slot attribute for styling and scripting hooks.
  • Focus-visible rings use the ring token, so keyboard focus is always visible.
  • Disabled and invalid states are conveyed to assistive tech, not by color alone.
  • Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
  • Labels go through __() and layout uses logical properties (ms-*, text-start), so it mirrors under dir="rtl" — flip the preview to RTL to confirm.
  • Dark mode uses the same semantic tokens under the dark class; high contrast follows forced-color system tokens.

Livewire

Needs wire:key

Add a stable wire:key when Livewire can reorder this interactive component.

livewire-component.blade.php Blade
<div wire:key="stepper-{{ $record->id }}">
    <div class="grid w-full gap-10 lg:grid-cols-2">
        <x-ui.stepper
            :steps="[
                ['title' => 'Install the package', 'description' => 'Add the dependency to your project.', 'status' => 'completed'],
                ['title' => 'Configure the registry', 'description' => 'Point the CLI at your registry endpoint.', 'status' => 'active'],
                ['title' => 'Add your first component', 'description' => 'Run ui:add to copy editable Blade in.', 'status' => 'default'],
            ]"
        />
    
        <x-ui.stepper orientation="horizontal" class="self-start">
            <x-ui.stepper.item step="1" title="Cart" status="completed" />
            <x-ui.stepper.item step="2" title="Shipping" status="active" />
            <x-ui.stepper.item step="3" title="Payment" status="default" :last="true" />
        </x-ui.stepper>
    
        {{-- numbered=false: a plain dot marker for a status timeline (the look
             formerly shipped as the separate timeline-steps component). --}}
        <x-ui.stepper :numbered="false" class="max-w-md">
            <x-ui.stepper.item status="completed" title="Order placed" description="Your order has been received and confirmed." />
            <x-ui.stepper.item status="active" title="Preparing shipment" description="Your items are being packed and labeled." />
            <x-ui.stepper.item status="default" title="Out for delivery" description="A courier will bring your package to your door." :last="true" />
        </x-ui.stepper>
    </div>
</div>

Source

The exact, editable files ui:add writes into your app. Previews render this same code; there are no preview-only components.

resources/views/components/ui/stepper.blade.php Blade
@props([
    'orientation' => 'vertical',
    'align' => 'start',
    'steps' => [],
    // false hides the step number/icon fallback and leaves a plain dot marker —
    // the timeline-steps look, now a mode of this component instead of a
    // second one. A completed step still shows its check either way.
    'numbered' => true,
    // An Alpine expression for the current 1-based step (`step`): every item
    // then follows it live — marker, connector, check and aria-current — so a
    // wizard never needs a second, hand-rolled rail beside the server one.
    'bind' => null,
    // Colour of the current and completed marks. primary (default) uses the
    // brand primary; neutral uses the foreground colour, so a product that
    // keeps primary for its one main action is not diluted by progress marks.
    'tone' => 'primary',
])

@php
    // Accept a string, a backed enum or a Stringable for `tone`.
    $styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
    $tone = $styles['normalizeVariant']($tone);
    $tone = in_array($tone, ['primary', 'neutral'], true) ? $tone : 'primary';
    $orientation = in_array($orientation, ['vertical', 'horizontal'], true) ? $orientation : 'vertical';
    $numbered = filter_var($numbered, FILTER_VALIDATE_BOOLEAN);

    // `align` only bites horizontally. `center` is the checkout/fulfilment shape:
    // each marker centred in its own column with the label beneath it and the
    // connector drawn marker-centre to marker-centre behind them.
    $align = in_array($align, ['start', 'center'], true) ? $align : 'start';

    // Array mode: render <x-ui.stepper.item> for each entry, auto-numbering and
    // flagging the last item so it omits its trailing connector. Slot mode lets
    // callers compose items by hand (with explicit `step`/`status`/nested content).
    $base = 'relative '.($orientation === 'horizontal' ? 'flex w-full' : 'flex flex-col');
@endphp

{{-- v1.1.0: CSS-only progress reveal. Server-rendered steps animate a one-shot
     reveal on load — completed connectors fill in (scaleX/Y on a pseudo-overlay,
     transform-only, no layout thrash) and the active marker pulses in with a
     ring. Purely decorative; fully gated behind prefers-reduced-motion. No JS. --}}
<style>
    @media (prefers-reduced-motion: no-preference) {
        @keyframes ui-stepper-fill-x {
            from { transform: scaleX(0); }
            to   { transform: scaleX(1); }
        }
        @keyframes ui-stepper-fill-y {
            from { transform: scaleY(0); }
            to   { transform: scaleY(1); }
        }
        @keyframes ui-stepper-pulse-in {
            0%   { transform: scale(0.85); box-shadow: 0 0 0 0 color-mix(in oklab, var(--ui-stepper-ring) calc(0.45 * 100%), transparent); }
            60%  { transform: scale(1); }
            100% { transform: scale(1); box-shadow: 0 0 0 0.375rem color-mix(in oklab, var(--ui-stepper-ring) calc(0 * 100%), transparent); }
        }

        /* The pulse ring takes the tone's colour. */
        [data-slot="stepper"] {
            --ui-stepper-ring: var(--primary-color, hsl(var(--primary)));
        }
        [data-slot="stepper"][data-tone="neutral"] {
            --ui-stepper-ring: var(--foreground-color, hsl(var(--foreground)));
        }

        /* Animate completed connectors growing toward the next step. The overlay
           inherits the connector's own border, so colour stays token-driven. */
        [data-slot="stepper"]:not([data-align="center"]) [data-status="completed"] [data-slot="stepper-connector"] {
            position: relative;
        }
        [data-slot="stepper"] [data-status="completed"] [data-slot="stepper-connector"]::after {
            content: "";
            position: absolute;
            inset: -1px;
            border: inherit;
            border-color: inherit;
            animation: ui-stepper-fill-x 0.5s ease-out both;
        }
        [data-slot="stepper"][data-orientation="vertical"] [data-status="completed"] [data-slot="stepper-connector"]::after {
            transform-origin: top;
            animation-name: ui-stepper-fill-y;
        }
        [data-slot="stepper"][data-orientation="horizontal"] [data-status="completed"] [data-slot="stepper-connector"]::after {
            transform-origin: inline-start;
        }

        /* Active marker pulses in with a fading ring in the tone's colour. */
        [data-slot="stepper"] [data-status="active"] [data-slot="stepper-marker"] {
            animation: ui-stepper-pulse-in 0.6s ease-out both;
        }
    }
</style>

<ol
    data-slot="stepper"
    data-orientation="{{ $orientation }}"
    data-align="{{ $align }}"
    data-tone="{{ $tone }}"
    @if ($bind !== null) data-bound="true" @endif
    {{ $attributes->merge(['class' => $base]) }}
>
    @if (! empty($steps))
        @foreach ($steps as $i => $step)
            <x-ui.stepper.item
                :step="$step['step'] ?? $i + 1"
                :title="$step['title'] ?? null"
                :description="$step['description'] ?? null"
                :status="$step['status'] ?? 'default'"
                :icon="$step['icon'] ?? null"
                :orientation="$orientation"
                :align="$align"
                :numbered="$numbered"
                :bind="$bind"
                :tone="$tone"
                :last="$loop->last"
            />
        @endforeach
    @else
        {{ $slot }}
    @endif
</ol>
resources/views/components/ui/stepper/item.blade.php Blade
@props([
    'step' => null,
    'title' => null,
    'description' => null,
    'status' => 'default',
    'icon' => null,
    'last' => false,
    'orientation' => 'vertical',
    'align' => 'start',
    'numbered' => true,
    'bind' => null,
    'tone' => 'primary',
])

@aware(['orientation' => 'vertical', 'align' => 'start', 'numbered' => true, 'bind' => null, 'tone' => 'primary'])

@php
    // timeline-steps' vocabulary (upcoming|current|complete) is accepted as a
    // backward-compatible alias for this component's own (default|active|completed).
    $status = ['upcoming' => 'default', 'current' => 'active', 'complete' => 'completed'][$status] ?? $status;
    $status = in_array($status, ['default', 'active', 'completed'], true) ? $status : 'default';
    $orientation = in_array($orientation, ['vertical', 'horizontal'], true) ? $orientation : 'vertical';
    $numbered = filter_var($numbered, FILTER_VALIDATE_BOOLEAN);
    $align = in_array($align, ['start', 'center'], true) ? $align : 'start';
    $isLast = filter_var($last, FILTER_VALIDATE_BOOLEAN);

    // Centred columns only exist horizontally; a vertical rail is always start-aligned.
    $isCentered = $orientation === 'horizontal' && $align === 'center';

    // Accept a string, a backed enum or a Stringable for `tone`.
    $styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
    $tone = $styles['normalizeVariant']($tone);
    // Full class strings per tone so Tailwind's JIT can see every utility.
    $tones = [
        'primary' => [
            'completed' => 'border-primary bg-primary text-primary-foreground',
            'active' => 'border-primary bg-background text-primary',
            'connector' => 'border-primary',
        ],
        'neutral' => [
            'completed' => 'border-foreground bg-foreground text-background',
            'active' => 'border-foreground bg-background text-foreground',
            'connector' => 'border-foreground',
        ],
    ];
    $tone = isset($tones[$tone]) ? $tone : 'primary';

    $markerStates = [
        'completed' => $tones[$tone]['completed'],
        'active' => $tones[$tone]['active'],
        'default' => 'border-border bg-background text-muted-foreground',
    ];
    $markerClasses = 'relative z-10 flex size-8 shrink-0 items-center justify-center rounded-full border text-sm font-medium transition-colors motion-reduce:transition-none '.$markerStates[$status];

    // Completed steps draw a "done" connector toward the next step.
    $doneConnector = $tones[$tone]['connector'];
    $connectorColor = $status === 'completed' ? $doneConnector : 'border-border';
    $connectorBase = 'transition-colors motion-reduce:transition-none '.$connectorColor;

    // Live mode: the stepper's `bind` names the current step in Alpine scope;
    // the status is derived from it per item and the same class maps apply.
    $live = $bind !== null && $step !== null;
    $liveStatus = $live ? "(({$bind}) > {$step} ? 'completed' : (({$bind}) === {$step} ? 'active' : 'default'))" : null;
    // Object-form class bindings, so the state a class set belongs to is the
    // only thing that keeps it on the element (a falsy entry removes the class
    // even when the server rendered it).
    $liveMarkerClass = $live ? sprintf(
        "{ '%s': (%s) > %d, '%s': (%s) === %d, '%s': (%s) < %d }",
        $markerStates['completed'], $bind, $step,
        $markerStates['active'], $bind, $step,
        $markerStates['default'], $bind, $step,
    ) : null;
    $liveConnectorClass = $live ? "{ '{$doneConnector}': ({$bind}) > {$step}, 'border-border': ({$bind}) <= {$step} }" : null;
    $liveTitleClass = $live ? "{ 'text-foreground': ({$bind}) >= {$step}, 'text-muted-foreground': ({$bind}) < {$step} }" : null;

    $itemLayout = match (true) {
        $isCentered => 'min-w-0 flex-1 flex-col items-center gap-2 text-center',
        $orientation === 'horizontal' => 'flex-1 flex-col items-start gap-2',
        default => 'gap-3',
    };

    $markerRow = match (true) {
        $isCentered => 'relative w-full items-center justify-center',
        $orientation === 'horizontal' => 'relative w-full items-center',
        default => 'relative flex-col items-center self-stretch',
    };
@endphp

<li
    data-slot="stepper-item"
    data-status="{{ $status }}"
    @if ($status === 'active') aria-current="step" @endif
    @if ($live)
        x-bind:data-status="{{ $liveStatus }}"
        x-bind:aria-current="({{ $bind }}) === {{ $step }} ? 'step' : false"
    @endif
    {{ $attributes->merge(['class' => 'relative flex '.$itemLayout]) }}
>
    <div class="flex {{ $markerRow }}">
        @if ($isCentered)
            {{-- One rail per gap, drawn from this marker's centre to the next one's.
                 The column is exactly one step wide, so `w-full` from the midpoint
                 lands on the neighbouring marker; the marker's own z-10 covers it. --}}
            @unless ($isLast)
                <span data-slot="stepper-connector" aria-hidden="true" class="absolute start-1/2 top-1/2 h-px w-full -translate-y-1/2 border-t {{ $connectorBase }}" @if ($live) x-bind:class="{{ $liveConnectorClass }}" @endif></span>
            @endunless
        @endif

        <span data-slot="stepper-marker" class="{{ $markerClasses }}" @if ($live) x-bind:class="{{ $liveMarkerClass }}" @endif>
            @if ($live)
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true" x-show="({{ $bind }}) > {{ $step }}" @if ($status !== 'completed') style="display: none;" @endif>
                    <path d="M20 6 9 17l-5-5" />
                </svg>
                <span class="sr-only" x-show="({{ $bind }}) > {{ $step }}" @if ($status !== 'completed') style="display: none;" @endif>{{ __('Completed') }}</span>
                <span x-show="({{ $bind }}) <= {{ $step }}" @if ($status === 'completed') style="display: none;" @endif>
                    @if ($icon !== null)
                        {{ $icon }}
                    @elseif ($numbered)
                        {{ $step }}
                    @endif
                </span>
            @elseif ($status === 'completed')
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true">
                    <path d="M20 6 9 17l-5-5" />
                </svg>
                {{-- The check is decorative; the state must also reach assistive tech
                     in text (WCAG 1.3.1, 1.4.1). The active step has aria-current. --}}
                <span class="sr-only">{{ __('Completed') }}</span>
            @elseif ($icon !== null)
                {{ $icon }}
            @elseif ($numbered)
                {{ $step }}
            @endif
        </span>

        @unless ($isLast || $isCentered)
            @if ($orientation === 'horizontal')
                <span data-slot="stepper-connector" aria-hidden="true" class="ms-2 h-px flex-1 border-t {{ $connectorBase }}" @if ($live) x-bind:class="{{ $liveConnectorClass }}" @endif></span>
            @else
                <span data-slot="stepper-connector" aria-hidden="true" class="mt-1 w-0 flex-1 border-s {{ $connectorBase }}" @if ($live) x-bind:class="{{ $liveConnectorClass }}" @endif></span>
            @endif
        @endunless
    </div>

    <div data-slot="stepper-content" class="{{ $orientation === 'vertical' ? 'pb-6' : ($isCentered ? 'min-w-0' : '') }}">
        @if ($title !== null)
            <div data-slot="stepper-title" class="font-medium leading-8 text-foreground" @if ($live) x-bind:class="{{ $liveTitleClass }}" @endif>{{ $title }}</div>
        @endif
        @if ($description !== null)
            <p data-slot="stepper-description" class="text-sm text-muted-foreground">{{ $description }}</p>
        @endif
        @if (trim($slot ?? '') !== '')
            <div class="mt-2 text-sm text-muted-foreground">{{ $slot }}</div>
        @endif
    </div>
</li>

Ownership & lifecycle

Owner, release state, review evidence and adoption for this item.
Owner
Platform UI (@JoshJML)
Current version
1.5.2
Status
Stable
License
open
Accessibility reviewed
No review date recorded
Last breaking change
No date recorded
Deprecation
Not deprecated
Contract
v6
Foundation
≥ 1.0.0