Stepper
A multi-step wizard component. Horizontal or vertical layout, 5 sizes, 8 colors, pending / active / completed states, linear gating, a bindable imperative api for external Next / Prev controls, and full roving keyboard navigation.
Basic Usage
Pass an items array and bind value. Click a step to advance.
<script lang="ts">
import { Stepper } from 'sv5ui';
const steps = [
{ value: 'account', title: 'Account', description: 'Email & password' },
{ value: 'profile', title: 'Profile', description: 'Tell us about you' },
{ value: 'preferences', title: 'Preferences', description: 'Notifications' },
{ value: 'done', title: 'Done', description: 'All set!' }
];
let current = $state<string | number>('account');
</script>
<Stepper items={steps} bind:value={current} />Vertical Orientation
Stack indicators in a column with the title and description to the side.
<Stepper items={steps} bind:value={current} orientation="vertical" />Sizes
5 size tokens scale the indicator and text sizes together.
xs
sm
md
lg
xl
<Stepper items={steps} size="xs" />
<Stepper items={steps} size="sm" />
<Stepper items={steps} size="md" />
<Stepper items={steps} size="lg" />
<Stepper items={steps} size="xl" />Colors
8 color tokens map active and completed indicators.
primary
secondary
tertiary
success
warning
error
info
surface
<Stepper items={steps} color="primary" defaultValue="profile" />
<Stepper items={steps} color="secondary" defaultValue="profile" />
<Stepper items={steps} color="tertiary" defaultValue="profile" />
<Stepper items={steps} color="success" defaultValue="profile" />
<Stepper items={steps} color="warning" defaultValue="profile" />
<Stepper items={steps} color="error" defaultValue="profile" />
<Stepper items={steps} color="info" defaultValue="profile" />
<Stepper items={steps} color="surface" defaultValue="profile" />Linear vs Free Navigation
With linear (default), users can only advance one step at a time by clicking. Already-completed steps remain freely clickable. With linear=false any step is clickable.
linear (default)
linear={false}
<!-- linear={true} (default): users can only advance one step at a time
by clicking. Completed steps remain freely clickable. -->
<Stepper items={steps} bind:value linear />
<!-- linear={false}: any step is clickable, anytime. -->
<Stepper items={steps} bind:value linear={false} />External Controls via API
Bind api to drive the Stepper from external buttons. next() and prev() bypass linear gating — wire your own validation before calling them.
<script lang="ts">
import { Stepper, Button } from 'sv5ui';
import type { StepperApi } from 'sv5ui';
let current = $state<string | number>('account');
let api = $state<StepperApi>();
</script>
<Stepper items={steps} bind:value={current} bind:api />
<!-- api.next() / api.prev() always bypass linear gating —
wire your own validation before calling them. -->
<div class="mt-4 flex justify-between">
<Button label="Back"
leadingIcon="lucide:chevron-left"
variant="outline"
disabled={!api?.hasPrev}
onclick={() => api?.prev()} />
<Button label={api?.hasNext ? 'Next' : 'Finish'}
trailingIcon={api?.hasNext ? 'lucide:chevron-right' : 'lucide:check'}
onclick={() => api?.hasNext ? api.next() : alert('Done!')} />
</div>Step Icons
Add item.icon to override the number/check fallback inside each indicator.
<!-- Each item.icon replaces the default number/check fallback. -->
<Stepper
defaultValue="payment"
items={[
{ value: 'cart', title: 'Cart', icon: 'lucide:shopping-cart' },
{ value: 'address', title: 'Address', icon: 'lucide:map-pin' },
{ value: 'payment', title: 'Payment', icon: 'lucide:credit-card' },
{ value: 'review', title: 'Review', icon: 'lucide:eye' }
]}
/>Disabled Steps
Disable a single step with item.disabled, or disable the whole Stepper with the root disabled prop.
<!-- Disable individual steps with item.disabled. -->
<Stepper
defaultValue="profile"
items={[
{ value: 'account', title: 'Account' },
{ value: 'profile', title: 'Profile' },
{ value: 'billing', title: 'Billing', disabled: true },
{ value: 'done', title: 'Done' }
]}
/>
<!-- Or disable the entire Stepper -->
<Stepper items={steps} disabled />Custom Indicator
The indicator snippet receives the lifecycle state — render differently for pending, active, and completed.
<!-- The indicator snippet receives { item, index, number, state, active }.
Use `state` ('pending' | 'active' | 'completed') to render differently. -->
<Stepper items={steps} bind:value={current}>
{#snippet indicator({ number, state })}
<span class="text-lg">
{state === 'completed' ? '✓' : state === 'active' ? '●' : number}
</span>
{/snippet}
</Stepper>Real-world: Signup Wizard
Combine Stepper with <Form> and <FormField> — each step is its own validated form, advancing via api.next() only when validation passes.
<script lang="ts">
import { Stepper, Form, FormField, Input, Button } from 'sv5ui';
import type { StepperApi } from 'sv5ui';
import * as v from 'valibot';
let api = $state<StepperApi>();
let value = $state<string | number>('account');
let formData = $state({ email: '', password: '', name: '' });
</script>
<Stepper
bind:value bind:api
items={[
{ value: 'account', title: 'Account', description: 'Email & password' },
{ value: 'profile', title: 'Profile', description: 'Your name' },
{ value: 'done', title: 'Done', description: 'Review & submit' }
]}
>
{#snippet body({ item })}
{#if item.value === 'account'}
<Form schema={v.object({ email: v.pipe(v.string(), v.email()), password: v.pipe(v.string(), v.minLength(8)) })}
bind:state={formData}
onsubmit={() => api?.next()}>
<FormField name="email" label="Email">
<Input bind:value={formData.email} type="email" />
</FormField>
<FormField name="password" label="Password">
<Input bind:value={formData.password} type="password" />
</FormField>
<Button type="submit" label="Continue" trailingIcon="lucide:chevron-right" />
</Form>
{:else if item.value === 'profile'}
<Form schema={v.object({ name: v.pipe(v.string(), v.minLength(2)) })}
bind:state={formData}
onsubmit={() => api?.next()}>
<FormField name="name" label="Full name">
<Input bind:value={formData.name} />
</FormField>
<div class="flex justify-between">
<Button label="Back" variant="outline" onclick={() => api?.prev()} />
<Button type="submit" label="Continue" trailingIcon="lucide:chevron-right" />
</div>
</Form>
{:else}
<p>Hi <strong>{formData.name}</strong>, ready to create your account?</p>
<div class="mt-4 flex justify-between">
<Button label="Back" variant="outline" onclick={() => api?.prev()} />
<Button label="Create account" color="success" />
</div>
{/if}
{/snippet}
</Stepper>StepperApi
Exposed via `bind:api` for imperative control.
| Member | Type |
|---|---|
next() | () => void |
prev() | () => void |
goTo(value) | (value) => void |
hasNext | boolean (readonly) |
hasPrev | boolean (readonly) |
activeIndex | number (readonly) |
UI Slots
Use the `ui` prop to override classes on internal elements.
| Slot | Description |
|---|---|
root | Outermost wrapper |
list | The ordered list of steps |
item | Per-step wrapper element |
trigger | Clickable area for each step |
container | Indicator + separator wrapper |
indicator | Circle showing the step number, icon, or check |
separator | Line connecting consecutive steps |
wrapper | Title + description text wrapper |
title | Step title text |
description | Step description text |
content | The active step's content panel |
Snippets
| Snippet | Description |
|---|---|
indicator | Replace the default number/check/icon. Receives `{ item, index, number, state, active }` |
titleSlot | Custom title renderer for every step |
descriptionSlot | Custom description renderer for every step |
body | Custom content for the active step's panel. Falls back to `item.content` |
Props
| Prop | Type | Default |
|---|---|---|
items | StepperItem[] | - |
value | string | number | - |
defaultValue | string | number | first item |
onValueChange | (value) => void | - |
api | StepperApi | - |
linear | boolean | true |
orientation | 'horizontal' | 'vertical' | 'horizontal' |
size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' |
color | 'primary' | 'secondary' | 'tertiary' | 'success' | 'warning' | 'error' | 'info' | 'surface' | 'primary' |
content | boolean | true |
disabled | boolean | false |
as | keyof HTMLElementTagNameMap | 'div' |
ref | HTMLElement | null | null |
class | string | - |
ui | Record<Slot, Class> | - |
Step Item Props
Each entry in the `items` array accepts these keys.
| Prop | Type | Default |
|---|---|---|
value | string | number | index |
title | string | - |
description | string | - |
icon | string | - |
disabled | boolean | false |
content | string | - |
class | string | - |
ui | Partial<Record<Slot, Class>> | - |