Stepper
Horizontal or vertical steps with animated progress and back-navigation.
$
pnpm dlx shadcn@latest add https://lodeui.com/r/stepper.jsonAccount
Email and password
Workspace
Name and URL
Invite
Add your team
Done
components/examples/stepper-demo.tsx
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Stepper } from "@/components/ui/stepper";
const steps = [
{ title: "Account", description: "Email and password" },
{ title: "Workspace", description: "Name and URL" },
{ title: "Invite", description: "Add your team" },
{ title: "Done" },
];
export default function StepperDemo() {
const [current, setCurrent] = React.useState(1);
return (
<div className="w-full max-w-xl space-y-8">
<Stepper steps={steps} current={current} onStepClick={setCurrent} />
<div className="flex justify-between">
<Button variant="ghost" disabled={current === 0} onClick={() => setCurrent((c) => c - 1)}>Back</Button>
<Button disabled={current === steps.length - 1} onClick={() => setCurrent((c) => c + 1)}>Continue</Button>
</div>
</div>
);
}When to use
- Multi-step forms: checkout, onboarding, setup wizards.
When not to use
- Tasks people can do in any order: use a checklist.
Installation
$
pnpm dlx shadcn@latest add https://lodeui.com/r/stepper.json1Install the dependencies
npm install motion lucide-react2Copy the source into your project
components/ui/stepper.tsx"use client"; import * as React from "react"; import { motion } from "motion/react"; import { Check } from "lucide-react"; import { cn } from "@/lib/utils"; export interface Step { title: string; description?: string; } export interface StepperProps { steps: Step[]; /** Zero-based index of the current step. */ current: number; orientation?: "horizontal" | "vertical"; /** Let people jump back to finished steps. */ onStepClick?: (index: number) => void; className?: string; } export function Stepper({ steps, current, orientation = "horizontal", onStepClick, className }: StepperProps) { const vertical = orientation === "vertical"; return ( <ol className={cn("flex", vertical ? "flex-col gap-0" : "items-start gap-2", className)}> {steps.map((step, i) => { const done = i < current; const active = i === current; const clickable = done && onStepClick; return ( <li key={step.title} aria-current={active ? "step" : undefined} className={cn("relative flex", vertical ? "gap-3 pb-6 last:pb-0" : "flex-1 flex-col gap-2")}> <div className={cn("flex items-center", vertical ? "flex-col" : "w-full")}> <button type="button" disabled={!clickable} onClick={() => clickable && onStepClick(i)} aria-label={`Step ${i + 1}: ${step.title}${done ? ", completed" : ""}`} className={cn( "relative grid size-7 shrink-0 place-items-center rounded-full border text-xs font-medium transition-colors", done && "border-primary bg-primary text-primary-foreground", active && "border-foreground text-foreground ring-4 ring-foreground/10", !done && !active && "border-border text-muted-foreground", clickable && "cursor-pointer hover:opacity-80", )} > {done ? ( <motion.span initial={{ scale: 0.4 }} animate={{ scale: 1 }} transition={{ type: "spring", bounce: 0.5 }}> <Check className="size-3.5" /> </motion.span> ) : ( i + 1 )} </button> {i < steps.length - 1 && ( <span className={cn("relative overflow-hidden bg-border", vertical ? "mt-1 w-px flex-1" : "mx-2 h-px flex-1")}> <motion.span className={cn("absolute bg-primary", vertical ? "inset-x-0 top-0" : "inset-y-0 left-0")} initial={false} animate={vertical ? { height: done ? "100%" : "0%" } : { width: done ? "100%" : "0%" }} transition={{ duration: 0.35, ease: [0.2, 0.8, 0.2, 1] }} /> </span> )} </div> <div className={cn(vertical ? "pt-0.5" : "pr-2")}> <p className={cn("text-sm font-medium", !done && !active && "text-muted-foreground")}>{step.title}</p> {step.description && <p className="mt-0.5 text-xs text-muted-foreground">{step.description}</p>} </div> </li> ); })} </ol> ); }
Usage
Import it from @/components/ui/stepper after installing.
example.tsx
import { Stepper } from "@/components/ui/stepper";
export function Checkout({ step, setStep }: { step: number; setStep: (n: number) => void }) {
return <Stepper current={step} onStepClick={setStep} steps={[{ title: "Cart" }, { title: "Shipping" }, { title: "Payment" }]} />;
}API reference
Stepper
Numbered steps with done, current and upcoming states.
| Prop | Type | Default | Description |
|---|---|---|---|
| steps | { title; description? }[] | – | The steps. |
| current | number | – | Zero-based index of the current step. |
| orientation | "horizontal" | "vertical" | "horizontal" | Layout. |
| onStepClick | (index: number) => void | – | Lets people go back to finished steps. |
Keyboard interactions
| Keys | Action |
|---|---|
| Tab | Move between finished steps (when clickable) |
| Enter | Go back to that step |
Accessibility
- An ordered list; the current step has aria-current="step".
Motion
- Connectors fill and check marks pop as steps complete.
Responsive behavior
- Use vertical on phones when titles are long.
Performance
- Static apart from the connector fill.
Notes for AI
- current is zero-based.
This page as Markdown, for assistants and coding agents:
/components/stepper/markdownThe full library index for assistants is at /llms.txt.
Lode222 doc pagesllms.txt