Navigation

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.

MemberType
next()() => void
prev()() => void
goTo(value)(value) => void
hasNextboolean (readonly)
hasPrevboolean (readonly)
activeIndexnumber (readonly)

UI Slots

Use the `ui` prop to override classes on internal elements.

SlotDescription
rootOutermost wrapper
listThe ordered list of steps
itemPer-step wrapper element
triggerClickable area for each step
containerIndicator + separator wrapper
indicatorCircle showing the step number, icon, or check
separatorLine connecting consecutive steps
wrapperTitle + description text wrapper
titleStep title text
descriptionStep description text
contentThe active step's content panel

Snippets

SnippetDescription
indicatorReplace the default number/check/icon. Receives `{ item, index, number, state, active }`
titleSlotCustom title renderer for every step
descriptionSlotCustom description renderer for every step
bodyCustom content for the active step's panel. Falls back to `item.content`

Props

PropTypeDefault
itemsStepperItem[]-
valuestring | number-
defaultValuestring | numberfirst item
onValueChange(value) => void-
apiStepperApi-
linearbooleantrue
orientation'horizontal' | 'vertical''horizontal'
size'xs' | 'sm' | 'md' | 'lg' | 'xl''md'
color'primary' | 'secondary' | 'tertiary' | 'success' | 'warning' | 'error' | 'info' | 'surface''primary'
contentbooleantrue
disabledbooleanfalse
askeyof HTMLElementTagNameMap'div'
refHTMLElement | nullnull
classstring-
uiRecord<Slot, Class>-

Step Item Props

Each entry in the `items` array accepts these keys.

PropTypeDefault
valuestring | numberindex
titlestring-
descriptionstring-
iconstring-
disabledbooleanfalse
contentstring-
classstring-
uiPartial<Record<Slot, Class>>-