StepIndicator
SM · 950B gzip budgetLinear step progress with current, completed, and pending states.
- Category
- Navigation
- Budget
- sm, single-purpose control
- External runtime
- React / React DOM only
axe fixtureSSR rendersource scanbundle budget
Preview
Live fixtureTry the component here. Locale-aware formatting and direction follow the selected language; example content and labels are not automatically translated. Overlay demos use interactive launchers.
Install
npx shadcn@latest add https://gear5-ui.vercel.app/r/step-indicator.jsonCopies the source into your project. Pulls in 1 shared registry item: cn.
New here? Set up Tailwind and import aliases first →Source & props
Edit on GitHub ↗Prop types and inline documentation are included below. This is repository source; the installer rewrites shared imports for your project.
import { cn } from "../lib/cn";
export interface StepIndicatorStep {
id: string;
label: string;
}
export interface StepIndicatorProps {
steps: StepIndicatorStep[];
currentId: string;
orientation?: "horizontal" | "vertical";
className?: string;
}
/**
* Progress indicator for multi-step flows. Each step communicates its status
* via `aria-current="step"` and an accessible name that includes the step
* number and state (complete, current, upcoming).
*/
export function StepIndicator({
steps,
currentId,
orientation = "horizontal",
className,
}: StepIndicatorProps) {
const currentIndex = steps.findIndex((s) => s.id === currentId);
return (
<nav aria-label="Progress" className={cn(orientation === "horizontal" ? "flex items-center" : "flex flex-col", className)}>
<ol
className={cn(
"flex",
orientation === "horizontal" ? "items-center gap-2" : "flex-col gap-4",
)}
role="list"
>
{steps.map((step, index) => {
const isComplete = index < currentIndex;
const isCurrent = step.id === currentId;
const isUpcoming = index > currentIndex;
return (
<li key={step.id} className={cn("flex", orientation === "horizontal" ? "items-center gap-2" : "items-start gap-3")}>
<span
aria-hidden="true"
className={cn(
"flex size-7 shrink-0 items-center justify-center rounded-full text-xs font-semibold",
isComplete && "bg-blue-600 text-white",
isCurrent && "border-2 border-blue-600 text-blue-700 dark:text-blue-400",
isUpcoming && "border-2 border-neutral-300 text-neutral-400 dark:border-neutral-700",
)}
>
{isComplete ? "✓" : index + 1}
</span>
<span className="flex flex-col">
<span
aria-current={isCurrent ? "step" : undefined}
className={cn(
"text-sm font-medium",
isUpcoming ? "text-neutral-400 dark:text-neutral-600" : "text-neutral-900 dark:text-neutral-100",
)}
>
{step.label}
</span>
<span className="sr-only">
{isComplete ? "Complete" : isCurrent ? "Current step" : "Upcoming"}
</span>
</span>
{orientation === "horizontal" && index < steps.length - 1 && (
<span
aria-hidden="true"
className={cn(
"h-px w-8",
isComplete ? "bg-blue-600" : "bg-neutral-200 dark:bg-neutral-800",
)}
/>
)}
</li>
);
})}
</ol>
</nav>
);
}
What CI checks
Quality contract- Bundled, minified and gzipped against its tier budget
- Audited by axe in the state previewed above
- Rendered through react-dom/server with no browser globals
- Scanned for network calls, dangerous sinks, and unguarded animation