Skip to content
omaris

Navigation

Stepper

Wizard progress — where you are in a checkout, an onboarding, a form too long for one screen.

import { Stepper } from 'omaris'
Learn

Examples

Basic

bind:step is the index. Steps behind you can be pressed to go back; steps ahead cannot be skipped to.

Step 3 of 4: Payment

Next: Review

<script lang="ts">	import { Stepper, StepperItem } from 'omaris';​	let step = $state(2);</script>​<Stepper bind:step label="Order progress" class="w-full max-w-3xl">	<StepperItem title="Cart" description="3 items" />	<StepperItem title="Shipping" description="Erbil" />	<StepperItem title="Payment" description="Card or cash" />	<StepperItem title="Review" /></Stepper>

Checkout

A three-step checkout. The stepper only says where you are; the form under it switches on step. A step that fails validation takes error, and in a column narrower than 36rem the row collapses to "Step 2 of 3 · Payment".

Step 1 of 3: Shipping

Next: Payment

<script lang="ts">	import {		Button,		DataList,		DataListItem,		Input,		Radio,		RadioGroup,		Stepper,		StepperItem,		Text	} from 'omaris';​	let step = $state(0);	let name = $state('');	let city = $state('');	let payment = $state('cash');	let tried = $state(false);​	const STEPS = 3;	const shippingValid = $derived(name.trim() !== '' && city.trim() !== '');	const placed = $derived(step === STEPS);​	function next() {		if (step === 0 && !shippingValid) {			tried = true;			return;		}		step += 1;	}</script>​<div class="flex w-full max-w-2xl flex-col gap-6">	<Stepper bind:step label="Checkout">		<StepperItem			title="Shipping"			description="Where it's going"			error={tried && !shippingValid ? 'Name and city needed' : false}		/>		<StepperItem title="Payment" description="Cash or card" />		<StepperItem title="Review" description="Check and place" />	</Stepper>​	{#if step === 0}		<div class="flex flex-col gap-4">			<Input label="Full name" bind:value={name} invalid={tried && !name.trim()} required />			<Input label="City" bind:value={city} invalid={tried && !city.trim()} required />		</div>	{:else if step === 1}		<RadioGroup label="Pay with" bind:value={payment}>			<Radio value="cash" label="Cash on delivery" />			<Radio value="card" label="Card" description="Visa or Mastercard" />		</RadioGroup>	{:else if step === 2}		<DataList dividers>			<DataListItem label="Name" value={name} />			<DataListItem label="City" value={city} />			<DataListItem label="Payment" value={payment === 'cash' ? 'Cash on delivery' : 'Card'} />		</DataList>	{:else}		<Text variant="title-md">Order placed — it's on its way to {city}.</Text>	{/if}​	<div class="flex justify-between gap-2">		<Button variant="text" disabled={step === 0 || placed} onclick={() => (step -= 1)}>Back</Button>		{#if placed}			<Button variant="tonal" onclick={() => ((step = 0), (tried = false))}>Start again</Button>		{:else}			<Button onclick={next}>{step === STEPS - 1 ? 'Place order' : 'Next'}</Button>		{/if}	</div></div>

Vertical

Vertical, each step's content opens under it. The content stays mounted while it is closed, so a half-filled form survives being stepped away from.

  1. One is enough to open with.

<script lang="ts">	import { Button, Input, Stepper, StepperItem, Text } from 'omaris';​	let step = $state(0);	let shop = $state('');</script>​<Stepper orientation="vertical" bind:step label="Open your shop" class="w-96 max-w-full">	<StepperItem title="Name your shop" description="You can change it later">		<div class="flex flex-col items-start gap-3">			<Input label="Shop name" bind:value={shop} class="w-full" />			<Button size="sm" onclick={() => (step = 1)}>Continue</Button>		</div>	</StepperItem>	<StepperItem title="Add a product">		<div class="flex flex-col items-start gap-3">			<Text variant="body-sm" tone="muted">One is enough to open with.</Text>			<div class="flex gap-2">				<Button size="sm" variant="text" onclick={() => (step = 0)}>Back</Button>				<Button size="sm" onclick={() => (step = 2)}>Continue</Button>			</div>		</div>	</StepperItem>	<StepperItem title="Go live" description={shop ? `${shop} opens to everyone` : undefined}>		<div class="flex gap-2">			<Button size="sm" variant="text" onclick={() => (step = 1)}>Back</Button>			<Button size="sm" onclick={() => (step = 3)}>Publish</Button>		</div>	</StepperItem></Stepper>

Overridden

linear={false} lets any step be picked, with complete marking the ones done out of order. collapse={false} keeps a narrow row a row, counter and upNext reword the collapsed view, and classes reaches every part.

2/3: Team

then Billing

<script lang="ts">	import { Stepper, StepperItem } from 'omaris';​	let section = $state(1);</script>​<div class="flex w-80 max-w-full flex-col gap-8">	<Stepper linear={false} collapse={false} bind:step={section} label="Profile">		<StepperItem title="About" complete classes={{ indicator: 'rounded-shape-sm' }} />		<StepperItem title="Photo" classes={{ indicator: 'rounded-shape-sm' }} />		<StepperItem title="Links" classes={{ indicator: 'rounded-shape-sm' }} />	</Stepper>​	<Stepper		step={1}		label="Onboarding"		counter={(current, total) => `${current}/${total}`}		upNext={(title) => `then ${title}`}		classes={{ summary: 'text-primary' }}	>		<StepperItem title="Account" />		<StepperItem title="Team" />		<StepperItem title="Billing" />	</Stepper></div>

When to use it

Use it for

  • A task split over a few screens — checkout, onboarding, a long form — where knowing how many steps are left is what keeps people going.
  • A wizard that must be done in order: linear (the default) lets people go back to a finished step but not skip ahead.
  • Sections that can be filled in any order: linear={false}, with complete on the ones that are done.
  • A setup flow in a narrow column: orientation="vertical", each step's content opening under it.

Not for

  • Switching between views of the same thing → Tabs.
  • The history of what already happened → Timeline.
  • A single wait with a known length → Progress.
  • Paging through results → Pagination.

Do

  • Keep it to three to five steps with one- or two-word titles; the collapsed view on a phone shows the title of the current one.
  • Drive it with bind:step and switch the content on the same value, with Back and Next Buttons under it.
  • Validate before moving on, and mark the step with error — a string is shown in place of its description.
  • Set step to the number of steps when the task is finished, so every step reads as complete.

Don't

  • Hide a required step behind disabled to force an order; that is what linear does.
  • Use it for a single form that fits on one screen — the steps are overhead.
  • Rely on the step circles alone for navigation on a phone; below 36rem they collapse to a summary, so Back and Next have to be there.

Quick reference

orientation
  • horizontal (default)
  • vertical

API

Stepper

Wizard progress — where you are in a checkout, an onboarding, a form too long for one screen.

Shaped like Tabs: the parent holds the position, each StepperItem is one step, and the order they are written in is the order they go in. bind:step is the index; the content under the stepper is yours to switch on it, so a step's form can live wherever the page wants it.

linear (the default) is the checkout rule: steps behind you can be revisited, steps ahead cannot be skipped to. Turn it off for a set of sections that can be filled in any order.

A horizontal stepper narrower than 36rem does not squeeze its titles until they truncate — it collapses to "Step 2 of 4 · Payment" over a progress bar. That is a container query, so a stepper in a narrow dialog on a wide screen collapses too.

import { Stepper } from 'omaris'
<Stepper bind:step label="Checkout">  <StepperItem title="Shipping" />  <StepperItem title="Payment" />  <StepperItem title="Review" /></Stepper>

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

root
No description in the source yet.
list
The <ol> of steps.
compact
The collapsed "Step 2 of 4" view of a narrow horizontal stepper.
summary
"Step 2 of 4 · Payment".
next
"Next: Review", at the end of the summary line.
progress
No description in the source yet.

Props

step bindable

Defaults to 0

number

The current step's index, from 0. Bindable. Set it to the number of steps to mark every one complete — the wizard is finished.

orientation

Defaults to 'horizontal'

'horizontal' | 'vertical'

A row across the top, or a column with each step's content under it.

horizontal
vertical
linear

Defaults to true

boolean

Steps behind the current one can be revisited; steps ahead cannot be skipped to. Off, any step can be picked at any time.

label

Defaults to 'Progress'

string

Accessible name for the list of steps — "Checkout".

collapse

Defaults to true

boolean

Let a horizontal stepper narrower than 36rem collapse to "Step 2 of 4 · Title" over a progress bar. Off, it stays a row and its titles truncate.

counter

Defaults to (current, total) => `Step ${current} of ${total}`

(step: number, total: number) => string

How the collapsed view counts: (2, 4) => 'Step 2 of 4'.

upNext

Defaults to (title) => `Next: ${title}`

(title: string) => string

How the collapsed view names what comes next: 'Review' => 'Next: Review'.

class
string
classes
StepperClasses

Per-part Tailwind overrides. class still covers the root.

children
Snippet

The StepperItems.

StepperItem

One step of a Stepper: a numbered indicator, a title, and the line to the next step, which fills in as the step is completed.

Its state is worked out from where it sits: before the current step it is complete, at it current, after it upcoming. error overrides all three, and complete marks a step done out of order in a non-linear stepper.

In a vertical stepper, children is the step's content, opened under the current step and closed under the others — mounted throughout, so a half-filled form survives being stepped away from.

import { StepperItem } from 'omaris'

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

root
No description in the source yet.
step
The pressable part: indicator, title and description.
indicator
The numbered circle. Its look follows the step's data-state, so the same selectors are there for a consumer's own styling.
body
No description in the source yet.
title
No description in the source yet.
description
No description in the source yet.
connector
The line to the next step.
fill
The part of the line that fills in as the step completes.
content
Vertical only: the step's content, opened while it is current.

Props

title required
string

The step's name — "Shipping". Also what the collapsed view shows.

description
string

A line under the title — "Where it's going".

error

Defaults to false

boolean | string

Something is wrong with this step: the indicator turns destructive and says so. A string is shown in place of the description.

complete
boolean

Mark the step done regardless of where the stepper is — a section of a non-linear form filled in out of order. Leave it unset and the position decides.

disabled

Defaults to false

boolean

The step cannot be picked, even when the stepper would allow it.

completeLabel

Defaults to 'completed'

string

Spoken after a completed step's title.

class
string
classes
StepperItemClasses

Per-part Tailwind overrides. class still covers the root.

icon
Snippet

An icon in place of the step's number. A completed step still shows its tick.

children
Snippet

Vertical only: the step's content, opened while it is the current step.