Stepper
Controlled multi-step wizard — a useStepper hook plus Stepper / StepperNav / StepperPanel with linear or non-linear flow, optional steps, and async validation gating.
Installation
npx shadcn@latest add https://ui.saasflare.io/r/stepper.jsonpnpm add @saasflare/uiUsage
"use client"import { Stepper, StepperPanel } from "@saasflare/ui"/** Uncontrolled wizard: items drive the indicator, the built-in nav handles Back / Next / Finish. */export function Demo() { return ( <Stepper items={[ { title: "Account", description: "Your login" }, { title: "Profile", description: "About you" }, { title: "Done", description: "All set" }, ]} > <StepperPanel value={0}> <div className="rounded-lg border border-border bg-card p-6"> <h3 className="text-base font-semibold text-foreground">Create your account</h3> <p className="mt-1 text-sm text-muted-foreground"> Pick a workspace name and an email to sign in with. </p> </div> </StepperPanel> <StepperPanel value={1}> <div className="rounded-lg border border-border bg-card p-6"> <h3 className="text-base font-semibold text-foreground">Tell us about you</h3> <p className="mt-1 text-sm text-muted-foreground"> Add a display name and avatar so teammates recognise you. </p> </div> </StepperPanel> <StepperPanel value={2}> <div className="rounded-lg border border-border bg-card p-6"> <h3 className="text-base font-semibold text-foreground">You're all set</h3> <p className="mt-1 text-sm text-muted-foreground"> Hit Finish to drop into your new workspace. </p> </div> </StepperPanel> </Stepper> )}Headless Hook
"use client"import { useStepper, Steps, Step, Button, CaretLeftIcon, CaretRightIcon, CheckIcon,} from "@saasflare/ui"const STEPS = [ { title: "Connect", description: "Link a repo" }, { title: "Configure", description: "Set env vars" }, { title: "Deploy", description: "Ship it" },] as const/** * Headless usage: `useStepper` drives the bare visual `Steps` indicator plus a * fully custom `Button` nav — no `<Stepper>` wrapper. The same hook composes * with any layout you build. */export function Demo() { const stepper = useStepper({ count: STEPS.length }) return ( <div className="flex flex-col gap-6"> <Steps current={stepper.activeStep}> {STEPS.map((step) => ( <Step key={step.title} title={step.title} description={step.description} /> ))} </Steps> <div className="rounded-lg border border-border bg-card p-6"> <h3 className="text-base font-semibold text-foreground"> {STEPS[stepper.activeStep].title} </h3> <p className="mt-1 text-sm text-muted-foreground"> Step {stepper.activeStep + 1} of {stepper.count} — driven entirely by the headless hook. </p> </div> <div className="flex items-center justify-between gap-3"> <Button variant="ghost" intent="neutral" startContent={<CaretLeftIcon aria-hidden="true" />} disabled={!stepper.canBack} onClick={() => stepper.back()} > Back </Button> <Button intent="primary" endContent={ stepper.isLast ? ( <CheckIcon aria-hidden="true" /> ) : ( <CaretRightIcon aria-hidden="true" /> ) } onClick={() => { void stepper.next() }} > {stepper.isLast ? "Deploy" : "Next"} </Button> </div> </div> )}Linear Validation
"use client"import { useState } from "react"import { Stepper, StepperPanel } from "@saasflare/ui"/** * Linear wizard with an async gate on step 1: Next runs a simulated 600ms check, * shows the loading state, and blocks with an inline error on failure. */export function Demo() { const [code, setCode] = useState("") // Resolves a validation result: pass when the code is "1234", else an error string. const validate = (step: number) => new Promise<boolean | string>((resolve) => { if (step !== 1) { resolve(true) return } setTimeout(() => { resolve(code.trim() === "1234" ? true : "Invalid code — try 1234.") }, 600) }) return ( <Stepper linear validate={validate} items={[ { title: "Email" }, { title: "Verify" }, { title: "Finish" }, ]} > <StepperPanel value={0}> <div className="rounded-lg border border-border bg-card p-6"> <h3 className="text-base font-semibold text-foreground">Enter your email</h3> <p className="mt-1 text-sm text-muted-foreground"> We'll send a one-time code to confirm it's you. </p> </div> </StepperPanel> <StepperPanel value={1}> <div className="rounded-lg border border-border bg-card p-6"> <h3 className="text-base font-semibold text-foreground">Verify the code</h3> <p className="mt-1 text-sm text-muted-foreground"> Type <span className="font-mono text-foreground">1234</span> to pass the gate; anything else is rejected after the check. </p> <input value={code} onChange={(event) => setCode(event.target.value)} inputMode="numeric" placeholder="Verification code" className="mt-4 w-40 rounded-md border border-border bg-background px-3 py-2 font-mono text-sm text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring" /> </div> </StepperPanel> <StepperPanel value={2}> <div className="rounded-lg border border-border bg-card p-6"> <h3 className="text-base font-semibold text-foreground">Verified</h3> <p className="mt-1 text-sm text-muted-foreground"> The gate passed — Finish to complete. </p> </div> </StepperPanel> </Stepper> )}Non Linear
"use client"import { Stepper, StepperPanel } from "@saasflare/ui"/** * Non-linear mode: every indicator step is a clickable trigger (roving focus, * goTo anywhere). An optional step shows the Skip button, and finished steps * keep their completed checkmark. */export function Demo() { return ( <Stepper linear={false} items={[ { title: "Plan", description: "Pick a tier" }, { title: "Add-ons", description: "Extras", optional: true }, { title: "Billing", description: "Payment" }, { title: "Review", description: "Confirm" }, ]} > <StepperPanel value={0}> <div className="rounded-lg border border-border bg-card p-6"> <h3 className="text-base font-semibold text-foreground">Choose a plan</h3> <p className="mt-1 text-sm text-muted-foreground"> Click any step in the indicator to jump straight there. </p> </div> </StepperPanel> <StepperPanel value={1}> <div className="rounded-lg border border-border bg-card p-6"> <h3 className="text-base font-semibold text-foreground">Add-ons</h3> <p className="mt-1 text-sm text-muted-foreground"> This step is optional — use Skip to move on without selecting anything. </p> </div> </StepperPanel> <StepperPanel value={2}> <div className="rounded-lg border border-border bg-card p-6"> <h3 className="text-base font-semibold text-foreground">Billing details</h3> <p className="mt-1 text-sm text-muted-foreground"> Completed steps keep a checkmark so you can navigate back freely. </p> </div> </StepperPanel> <StepperPanel value={3}> <div className="rounded-lg border border-border bg-card p-6"> <h3 className="text-base font-semibold text-foreground">Review & confirm</h3> <p className="mt-1 text-sm text-muted-foreground"> Everything looks good — Finish to subscribe. </p> </div> </StepperPanel> </Stepper> )}API Reference
Stepper
Prop
Type
StepperPanel
Prop
Type
StepperNav
Prop
Type
StepperContent
Prop
Type
Sidebar
Collapsible application sidebar system — provider, rail, header/footer, groups, and a full menu (button, action, badge, sub-menu, skeleton) with cookie-persisted open state and a ⌘B keyboard shortcut.
Steps
Horizontal or vertical step indicator showing progress through a multi-stage flow or wizard.