Saasflare UI
Components

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.json
pnpm add @saasflare/ui

Usage

Loading…
"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

Loading…
"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

Loading…
"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

Loading…
"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 &amp; 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

On this page