Stepper
Shows progress through a sequence of steps, with active, complete, and inactive states.
npx shadcn@latest add @velaui/stepperpnpm dlx velaui add @velaui/stepperyarn dlx shadcn@latest add @velaui/stepperbunx --bun shadcn@latest add @velaui/stepperveni add stepperInstalls the copy of vela-ui that veni-builder serves to Venizia apps; veni-builder serves the Radix UI copy only, so this tab exists for that base alone.
npx shadcn@latest add @velaui/stepperpnpm dlx velaui add @velaui/stepperyarn dlx shadcn@latest add @velaui/stepperbunx --bun shadcn@latest add @velaui/stepperExamples
- Default
- Optional
- Vertical
- Center
Usage
Props
| Prop | Type | Default |
|---|---|---|
currentrequired | number | — |
label | "center" | "row" | "row" |
orientation | "horizontal" | "vertical" | "horizontal" |
stepsrequired | readonly { title: string; optional?: string; }[] | — |
Takes these and every prop of StepperProps.
Dependencies
- npm
class-variance-authority@0.7.1- Installs with it
- 1 icon
Source
components/ui/stepper.tsx
/* eslint-disable react-refresh/only-export-components */
"use client";
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { Check01 } from "@/components/icons/check-01";
import { cn } from "@/lib/utils";
/**
* Stepper — a linear progress indicator (VELA-30).
*
* The design crosses `State` (Active / Inactive / Complete) with `Label`
* (Row = beside the disc, Center = stacked under it) plus an optional caption
* per step. Discs are 32px pills: active = primary fill with an 18px semibold
* H4 numeral, inactive = surface-1 with a disabled numeral, complete = success
* fill with a white check. The connector between steps is a 1px neutral rule.
*/
const stepperVariants = cva("flex", {
variants: {
orientation: {
horizontal: "flex-row items-start",
vertical: "flex-col items-start",
},
label: {
row: "items-center",
center: "items-start",
},
},
defaultVariants: {
orientation: "horizontal",
label: "row",
},
});
export type StepperProps = React.ComponentProps<"ol"> &
VariantProps<typeof stepperVariants> & {
/**
* One entry per step. `current` marks the active index; every step before
* it renders as complete (success disc + check), every step after as
* inactive.
*/
steps: ReadonlyArray<{ title: string; optional?: string }>;
current: number;
};
function Stepper({
className,
steps,
current,
orientation = "horizontal",
label = "row",
...props
}: StepperProps) {
const resolvedOrientation = orientation ?? "horizontal";
const resolvedLabel = label ?? "row";
return (
<ol
data-slot="stepper"
data-orientation={resolvedOrientation}
data-label={resolvedLabel}
className={cn(
stepperVariants({ orientation, label }),
"m-0 list-none p-0",
className,
)}
{...props}
>
{steps.map((step, index) => {
const state = index === current ? "active" : index < current ? "complete" : "inactive";
return (
<StepperItem
key={`${index}-${step.title}`}
state={state}
stepNumber={index + 1}
title={step.title}
optional={step.optional}
orientation={resolvedOrientation}
label={resolvedLabel}
/>
);
})}
</ol>
);
}
const stepperItemVariants = cva("flex", {
variants: {
orientation: {
horizontal: "flex-row items-start",
vertical: "flex-col items-stretch",
},
},
defaultVariants: {
orientation: "horizontal",
},
});
const stepperDiscVariants = cva(
"relative flex size-8 shrink-0 items-center justify-center rounded-full text-center text-lg leading-6 font-semibold tracking-[-0.18px] select-none",
{
variants: {
state: {
active: "bg-primary-default text-text-inverse",
inactive: "bg-bg-surface-1 text-text-disabled",
complete: "bg-success-middle text-base-white",
},
},
defaultVariants: {
state: "inactive",
},
},
);
type StepperItemProps = {
state: "active" | "inactive" | "complete";
stepNumber: number;
title: string;
optional?: string;
orientation: "horizontal" | "vertical";
label: "row" | "center";
};
function StepperItem({
state,
stepNumber,
title,
optional,
orientation,
label,
}: StepperItemProps) {
return (
<li
data-slot="stepper-item"
data-state={state}
data-orientation={orientation}
data-label={label}
className={cn(
stepperItemVariants({ orientation }),
"relative",
// Connector: flex-1 rule between items (horizontal = right of every
// item except the last; vertical = below, except the last).
orientation === "horizontal" &&
"not-last:after:mt-4 not-last:after:h-px not-last:after:flex-1 not-last:after:bg-border-default",
orientation === "vertical" &&
"not-last:after:ml-4 not-last:after:w-px not-last:after:self-stretch not-last:after:bg-border-default",
)}
>
<div
className={cn(
"flex",
orientation === "horizontal" && "items-center",
label === "center" && "flex-col items-center",
label === "row" && "gap-3",
label === "center" && orientation === "vertical" && "items-start",
)}
>
<span data-slot="stepper-disc" className={stepperDiscVariants({ state })}>
{state === "complete" ? (
<Check01 className="size-4" />
) : (
stepNumber
)}
</span>
<span
className={cn(
"flex flex-col",
label === "row" ? "gap-0.5" : "gap-0.5 text-center",
)}
>
<span
data-slot="stepper-title"
className={cn(
"text-base leading-[1.4] font-semibold text-text-strong",
state === "inactive" && "text-text-note",
)}
>
{title}
</span>
{optional && (
<span
data-slot="stepper-optional"
className="text-xs leading-4 text-text-note"
>
{optional}
</span>
)}
</span>
</div>
</li>
);
}
export { Stepper, stepperVariants };