Gesso
Navigation

Stepper

An inchworm rail for multi-step flows. Every step is numbered, finished steps draw a check, and a worm stretches into the next one.

A step indicator without contained nodes. Each step is a pill on a rail in equal columns, with its number and name. Finished pills are solid and their number turns into a CheckMark that draws itself, upcoming steps sit on the track and read grayed out, and the current step carries the worm. When you advance, the worm's head runs into the next pill and the tail catches up, the same stretch the Tabs line uses. Finish the flow and the worm covers the whole rail, so the gaps close into one bar.

With onValueChange, every finished step is a button, so you can go back to any of them. Upcoming steps render as disabled buttons: visible and in reading order, but not clickable until you get there.

Vertical

orientation="vertical" stands the rail beside the numbered names, for a setup sidebar. The current pill fills its row, and the worm walks down the list.

Scope

Finished steps are buttons. Click one to go back to it.

Compact

The rail fills its container, so a width class makes it a smaller indicator. Names truncate before the numbers do.

On a rule

variant="rule" is for a PageHeader's tabs slot, where a flow replaces a tab row. It is the same 40px row as TabsList and draws the same bottom hairline, so the header does not move when one swaps for the other. The pills drop to 2px and sit on the rule, the worm matches the Tabs mark, and each step's number and name sit above its pill where a tab label would. Toggle the header between the two:

New initiative

Draft

Set up the brief before it goes to the team

Scope goes here.

Motion

One moment: the worm. The check is its follow-through.

RuleWhat happens
AdvanceHead runs to the next pill in --duration-fast; the tail follows in --duration-base after 80ms. Both --ease-out.
CheckThe finished step's number fades as the tail leaves, and its check draws on the same 80ms delay.
BackMirrors: the head runs backward and the tail follows. The check clears at once.
CompleteThe worm spans first pill to last, covering the gaps.
Mount and resizeSnap, no play-in. Checks already done render drawn.
Reduced motionSnap. Checks appear without drawing.
InterruptA new value retargets both edges mid-flight.

Accessibility

The stepper is a nav around an ordered list. The current item carries aria-current="step", and every item reads as Step 2 of 5: Scope, current. The visual number and check are hidden from assistive tech so the step is not counted twice. With onValueChange, finished steps are buttons with a focus ring and upcoming steps are disabled buttons. State never rides on color alone: finished steps show a check and a solid pill, the current step carries the worm and a heavier name, and forced colors map them to CanvasText, Highlight, and GrayText.

Usage

import { Stepper } from '@/components/ui/stepper';

const steps = [{ label: 'Basics' }, { label: 'Scope' }, { label: 'Review' }];

<Stepper steps={steps} value={step} onValueChange={setStep} aria-label="New initiative" />
PropTypeDefaultNotes
steps{ label: string; id?: string }[]id keys the item when labels repeat.
valuenumberIndex of the current step. steps.length means complete.
onValueChange(index: number) => voidMakes finished steps clickable and upcoming ones disabled buttons.
orientation"horizontal" | "vertical""horizontal"
variant"rail" | "rule""rail"rule rides a bottom hairline for a PageHeader tabs slot. Horizontal only.
aria-labelstring"Progress"Name the flow.

Install

shadcn add @gesso/stepper

On this page