Skip to content
Navigation

Stepper

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

Walkthrough

1. 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>

2. 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>

3. 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>

4. 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>