Navigation
Stepper
Wizard progress — where you are in a checkout, an onboarding, a form too long for one screen.
import { Stepper } from 'omaris' 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.
-
-
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}, withcompleteon 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:stepand 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
stepto the number of steps when the task is finished, so every step reads as complete.
Don't
- Hide a required step behind
disabledto force an order; that is whatlineardoes. - 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 bindableDefaults 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.
horizontalvertical
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.