An ordered progress sequence with current-step semantics and optional navigation to completed steps.
- Invite your teamAdd collaborators
- Review and launchFinal check
import { ProgressSteps } from "@/components/ballmac/progress-steps"
const steps = [
{
id: "details",
label: "Workspace details",
description: "Name and purpose",
},
{ id: "team", label: "Invite your team", description: "Add collaborators" },
{ id: "finish", label: "Review and launch", description: "Final check" },
]
export default function ProgressStepsDemo() {
return (
<ProgressSteps
className="w-full max-w-xs"
label="Workspace setup"
steps={steps}
defaultActiveIndex={1}
navigable
/>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/progress-stepsInstall the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18nCopy the source into your project.
components/ballmac/progress-steps.tsx// Ballmac UI: Progress Steps. https://ui.ballmac.com/components/progress-steps "use client" import * as React from "react" import { Check } from "lucide-react" import { cn } from "@/lib/utils" import { useMessages } from "@/lib/ballmac/i18n" type ProgressStep = { /** Stable step ID. */ id: string /** Visible step label. */ label: string /** Optional short description. */ description?: string } type ProgressStepsProps = Omit<React.ComponentProps<"ol">, "children"> & { /** Ordered steps in the process. */ steps: ProgressStep[] /** Controlled active step index. */ activeIndex?: number /** Initially active step index. */ defaultActiveIndex?: number /** Called when a navigable step is selected. */ onActiveIndexChange?: (index: number) => void /** Allow returning to completed steps with a button. */ navigable?: boolean /** Accessible name for the sequence. */ label?: string } function ProgressSteps({ className, steps, activeIndex, defaultActiveIndex = 0, onActiveIndexChange, navigable = false, label, ...props }: ProgressStepsProps) { const msg = useMessages() label ??= msg("progress-steps.label", "Progress") const [internal, setInternal] = React.useState(defaultActiveIndex) const active = Math.max( 0, Math.min(steps.length - 1, activeIndex ?? internal), ) function choose(index: number) { if (activeIndex === undefined) setInternal(index) onActiveIndexChange?.(index) } return ( <ol data-slot="progress-steps" aria-label={label} className={cn("grid w-full min-w-0 gap-2", className)} {...props} > {steps.map((step, index) => { const complete = index < active const current = index === active const content = ( <> <span aria-hidden="true" className={cn( "flex size-7 shrink-0 items-center justify-center rounded-full border text-xs font-semibold tabular-nums", complete && "border-primary bg-primary text-primary-foreground", current && "border-primary text-primary", !complete && !current && "border-border bg-muted text-muted-foreground", )} > {complete ? <Check className="size-3.5" /> : index + 1} </span> <span className="min-w-0 text-start"> <span className={cn( "block text-sm font-medium", !complete && !current && "text-muted-foreground", )} > {step.label} </span> {step.description && ( <span className="text-muted-foreground block text-xs"> {step.description} </span> )} </span> </> ) return ( <li key={step.id} data-slot="progress-steps-item" className="relative min-w-0 pb-2 before:absolute before:top-8 before:bottom-0 before:start-3.5 before:w-px before:bg-border last:pb-0 last:before:hidden" > {navigable && complete ? ( <button type="button" aria-label={msg("progress-steps.returnTo", "Return to {label}", { label: step.label })} onClick={() => choose(index)} className="hover:bg-accent relative flex min-h-9 w-full items-center gap-3 rounded-md px-1 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50" > {content} </button> ) : ( <div aria-current={current ? "step" : undefined} className="relative flex min-h-9 items-center gap-3 px-1" > {content} </div> )} </li> ) })} </ol> ) } export { ProgressSteps, type ProgressStepsProps, type ProgressStep }Update the import paths to match your project setup.
Usage
import { ProgressSteps } from "@/components/ballmac/progress-steps"The full example is in the Code tab above.
Examples
States and variants
- Draft
- Review
- Publish
import { ProgressSteps } from "@/components/ballmac/progress-steps"
export default function ProgressStepsStates() {
return (
<ProgressSteps
className="w-full max-w-xs"
label="Publishing progress"
steps={[
{ id: "draft", label: "Draft" },
{ id: "review", label: "Review" },
{ id: "publish", label: "Publish" },
]}
activeIndex={2}
/>
)
}API reference
| Prop | Type | Default |
|---|---|---|
steps*Ordered steps in the process. | ProgressStep[] | — |
activeIndexControlled active step index. | number | — |
defaultActiveIndexInitially active step index. | number | 0 |
onActiveIndexChangeCalled when a navigable step is selected. | (index: number) => void | — |
navigableAllow returning to completed steps with a button. | boolean | false |
labelAccessible name for the sequence. | string | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab / Enter | Return to a completed step when navigable |
Use with AI
An ordered progress sequence with current-step semantics and optional navigation to completed steps. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Progress Steps (@ballmac/progress-steps) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Show a multi-step flow
- Let users revisit completed steps
Not for
- Use progress for an unlabelled percentage
Registry JSON: https://ui.ballmac.com/r/progress-steps.json
Credits
Free to use in personal and commercial projects.
- npm
- lucide-react
- Registry
- @ballmac/i18nshadcn/utils
Pairs well with
Stepper Form
A guided multistep form shell with gated progression, review states, and keyboard-accessible navigation.
Progress
A Radix progress bar with clamped values, optional percentage text, and a reduced-motion-safe pending state.
Alert Dialog
Focus-managed confirmation for consequential actions, with a clear cancel path, optional media, and a token-based destructive action.
Alert
A semantic callout with five theme-aware tones, clear icon placement, an action row, and opt-in urgent announcements.