Overlays & navigation
Stepper
A stepper shows progress through a multi-step flow: onboarding, checkout, an import. Steps are complete, current, upcoming or in error, derived from a single current index unless you set a status yourself. It lays out horizontally for short flows and vertically when steps carry their own content, and it renders on the server.
- Account (Completed)
- Workspace (Current step)
- Invite team (Not started)
- Billing (Not started)
Step 2 of 4
const onboarding: Step[] = [ { title: "Account", description: "Name and password" }, { title: "Workspace", description: "Company details" }, { title: "Invite team", description: "Optional" }, { title: "Billing", description: "Plan and payment" },]; export function Onboarding() { const [current, setCurrent] = useState(1); const done = current >= onboarding.length; return ( <div className="mx-auto flex w-full max-w-2xl flex-col gap-8"> <Stepper steps={onboarding} current={current} label="Set up Ledgerly" /> <div className="flex items-center justify-between gap-3 border-t border-line pt-5"> <Button variant="ghost" iconLeft={<ArrowLeft strokeWidth={1.75} />} disabled={current === 0} onClick={() => setCurrent((c) => c - 1)}> Back </Button> <span className="text-[13px] text-muted tabular">{done ? "All done" : `Step ${current + 1} of ${onboarding.length}`}</span> {done ? ( <Button variant="outline" onClick={() => setCurrent(0)}>Start over</Button> ) : ( <Button variant="primary" iconRight={current === onboarding.length - 1 ? <Check strokeWidth={1.75} /> : <ArrowRight strokeWidth={1.75} />} onClick={() => setCurrent((c) => c + 1)} > {current === onboarding.length - 1 ? "Finish" : "Continue"} </Button> )} </div> </div> );}Usage
components/ui/stepper.tsx
import { Stepper } from "@/components/ui/stepper"; <Stepper current={1} steps={[ { title: "Account" }, { title: "Workspace" }, { title: "Billing" }, ]}/>Examples
Horizontal
Drive current from your flow's state. Descriptions are hidden on phones to keep every step on one line.
- Account (Completed)
- Workspace (Current step)
- Invite team (Not started)
- Billing (Not started)
Step 2 of 4
const onboarding: Step[] = [ { title: "Account", description: "Name and password" }, { title: "Workspace", description: "Company details" }, { title: "Invite team", description: "Optional" }, { title: "Billing", description: "Plan and payment" },]; export function Onboarding() { const [current, setCurrent] = useState(1); const done = current >= onboarding.length; return ( <div className="mx-auto flex w-full max-w-2xl flex-col gap-8"> <Stepper steps={onboarding} current={current} label="Set up Ledgerly" /> <div className="flex items-center justify-between gap-3 border-t border-line pt-5"> <Button variant="ghost" iconLeft={<ArrowLeft strokeWidth={1.75} />} disabled={current === 0} onClick={() => setCurrent((c) => c - 1)}> Back </Button> <span className="text-[13px] text-muted tabular">{done ? "All done" : `Step ${current + 1} of ${onboarding.length}`}</span> {done ? ( <Button variant="outline" onClick={() => setCurrent(0)}>Start over</Button> ) : ( <Button variant="primary" iconRight={current === onboarding.length - 1 ? <Check strokeWidth={1.75} /> : <ArrowRight strokeWidth={1.75} />} onClick={() => setCurrent((c) => c + 1)} > {current === onboarding.length - 1 ? "Finish" : "Continue"} </Button> )} </div> </div> );}Vertical with an error
Vertical steps can hold content under the title. A step with status="error" stays red until it is fixed.
- Upload file (Completed)clients-2024.csv · 214 rows
- Match columns (Completed)8 of 8 columns matched
- Check data (Needs attention)3 rows have no email address.Rows 18, 52 and 140: email is required
- Import (Not started)Create 214 clients
<Stepper orientation="vertical" label="Import clients" current={2} className="mx-auto w-full max-w-md" steps={[ { title: "Upload file", description: "clients-2024.csv · 214 rows" }, { title: "Match columns", description: "8 of 8 columns matched" }, { title: "Check data", status: "error", description: "3 rows have no email address.", content: ( <div className="rounded-ui border border-danger/25 bg-danger/5 px-3 py-2 font-mono text-[12px] leading-5 text-danger"> Rows 18, 52 and 140: email is required </div> ), }, { title: "Import", description: "Create 214 clients" }, ]}/>States
Complete steps show a check on the accent, the current step a ring, upcoming steps a quiet outline and errors a cross. Each state is also spelled out for screen readers.
- Cart (Completed)
- Shipping (Completed)
- Payment (Needs attention)
- Review (Not started)
<div className="mx-auto w-full max-w-xl"> <Stepper current={2} steps={[{ title: "Cart" }, { title: "Shipping" }, { title: "Payment", status: "error" }, { title: "Review" }]} /></div>Props
Stepper
PropTypeDefaultDescription
stepsrequiredStep[]default: —{ title, description?, status?, content? }[] in order.currentnumberdefault: 0Index of the current step. Earlier steps are complete, later ones upcoming. Use steps.length when all are done.orientation"horizontal" | "vertical"default: "horizontal"Row of steps, or a column with room for content.labelstringdefault: "Progress"Accessible name of the list.classNamestringdefault: —Classes for the list element.
Step
PropTypeDefaultDescription
titlerequiredstringdefault: —Short name of the step.descriptionstringdefault: —Second line. Hidden on phones in the horizontal layout.status"complete" | "current" | "upcoming" | "error"default: —Overrides the status derived from current.contentReactNodedefault: —Extra content under the step (vertical only).
Accessibility
- Rendered as an ordered list with an accessible name, so screen readers announce the number of steps.
- The current step has aria-current="step".
- Each step's state is written out in visually hidden text (“Completed”, “Current step”, “Needs attention”), so it never relies on color or icons alone.
- The step indicators and connecting lines are decorative and hidden from assistive tech.