Stepper
A component that guides users through a multi-step process with visual progress indication.
Overview
The Stepper component guides users through sequential steps with separate
selection and status states. A step can be inactive, active, or disabled while
also using the incomplete, completed, or loading status. Previous and current
steps are progress-colored separately from the status icon. It supports
horizontal and vertical layouts, controlled and uncontrolled values, animated
step content, and optional URL state.
Stepper indicators are surface-aware. Unreached indicators land two levels above the parent surface with the matching contextual border. Reached indicators use the brand fill, while status controls whether the marker shows a number, check icon, or spinner. Hovering a step increases the indicator and the wrapper around the title/description without requiring local color overrides.
Usage
import {
Stepper,
StepperDescription,
StepperHeading,
StepperIndicator,
StepperItem,
StepperSeparator,
StepperTitle,
StepperTrigger,
} from "@tilt-legal/cubitt-components/stepper";<Stepper defaultValue={1}>
<StepperItem step={1}>
<StepperTrigger>
<StepperIndicator />
<StepperHeading>
<StepperTitle>Account</StepperTitle>
<StepperDescription>Create your account</StepperDescription>
</StepperHeading>
</StepperTrigger>
<StepperSeparator />
</StepperItem>
<StepperItem step={2}>
<StepperTrigger>
<StepperIndicator />
<StepperHeading>
<StepperTitle>Profile</StepperTitle>
<StepperDescription>Add your profile details</StepperDescription>
</StepperHeading>
</StepperTrigger>
</StepperItem>
</Stepper>URL State
Provide paramName to sync the active step with the current URL.
<Stepper defaultValue={1} paramName="step">
<StepperItem step={1}>
<StepperTrigger>
<StepperIndicator />
<StepperHeading>
<StepperTitle>Account</StepperTitle>
</StepperHeading>
</StepperTrigger>
</StepperItem>
<StepperItem step={2}>
<StepperTrigger>
<StepperIndicator />
<StepperHeading>
<StepperTitle>Profile</StepperTitle>
</StepperHeading>
</StepperTrigger>
</StepperItem>
<StepperItem step={3}>
<StepperTrigger>
<StepperIndicator />
<StepperHeading>
<StepperTitle>Complete</StepperTitle>
</StepperHeading>
</StepperTrigger>
</StepperItem>
</Stepper>Examples
States
Toggle one centered step through selection state and status.
import { useState } from "react";
import {
Stepper,
StepperDescription,
StepperHeading,
StepperIndicator,
StepperItem,
StepperTitle,
StepperTrigger,
} from "@tilt-legal/cubitt-components/stepper";
import {
ToggleGroup,
ToggleGroupItem,
} from "@tilt-legal/cubitt-components/toggle-group";
type SelectionState = "inactive" | "active" | "disabled";
type Status = "incomplete" | "completed" | "loading";
const selectionStates = ["inactive", "active", "disabled"] as const;
const statuses = ["incomplete", "completed", "loading"] as const;
const [selectionState, setSelectionState] = useState<SelectionState>("active");
const [status, setStatus] = useState<Status>("incomplete");
const activeStep = selectionState === "active" ? 1 : 0;
<>
<Stepper
className="justify-center"
orientation="horizontal"
value={activeStep}
>
<StepperItem
className="flex-col!"
completed={status === "completed"}
disabled={selectionState === "disabled"}
loading={status === "loading"}
step={1}
>
<StepperTrigger className="flex-col gap-3 rounded">
<StepperIndicator />
<StepperHeading className="space-y-0.5 text-center">
<StepperTitle>
{`${selectionState[0]?.toUpperCase()}${selectionState.slice(1)}`}
</StepperTitle>
<StepperDescription>
{`${status[0]?.toUpperCase()}${status.slice(1)} status`}
</StepperDescription>
</StepperHeading>
</StepperTrigger>
</StepperItem>
</Stepper>
<PreviewFooter className="flex flex-wrap items-center justify-center gap-2">
<ToggleGroup
aria-label="Stepper selection state"
groupVariant="segmented"
multiple={false}
onValueChange={(value) => {
if (selectionStates.includes(value as SelectionState)) {
setSelectionState(value as SelectionState);
}
}}
value={selectionState}
>
<ToggleGroupItem value="inactive">Inactive</ToggleGroupItem>
<ToggleGroupItem value="active">Active</ToggleGroupItem>
<ToggleGroupItem value="disabled">Disabled</ToggleGroupItem>
</ToggleGroup>
<ToggleGroup
aria-label="Stepper status"
groupVariant="segmented"
multiple={false}
onValueChange={(value) => {
if (statuses.includes(value as Status)) {
setStatus(value as Status);
}
}}
value={status}
>
<ToggleGroupItem value="incomplete">Incomplete</ToggleGroupItem>
<ToggleGroupItem value="completed">Completed</ToggleGroupItem>
<ToggleGroupItem value="loading">Loading</ToggleGroupItem>
</ToggleGroup>
</PreviewFooter>
</>;Basic
<Stepper defaultValue={2}>
{[1, 2, 3].map((step) => (
<StepperItem className="relative flex-1 flex-col!" key={step} step={step}>
<StepperTrigger className="flex-col gap-3 rounded">
<StepperIndicator />
<StepperHeading className="space-y-0.5 text-center">
<StepperTitle>{`Step ${step}`}</StepperTitle>
<StepperDescription className="max-sm:hidden">
{`Description for step ${step}`}
</StepperDescription>
</StepperHeading>
</StepperTrigger>
{step < 3 && (
<StepperSeparator className="absolute inset-x-0 top-3 left-[calc(50%+0.75rem+0.125rem)] -order-1 m-0 -translate-y-1/2 group-data-[orientation=horizontal]/stepper:w-[calc(100%-1.5rem-0.25rem)] group-data-[orientation=horizontal]/stepper:flex-none" />
)}
</StepperItem>
))}
</Stepper>Vertical
<Stepper defaultValue={2} orientation="vertical">
{[1, 2, 3].map((step) => (
<StepperItem className="relative not-last:flex-1 items-start" key={step} step={step}>
<StepperTrigger className="rounded pb-12 last:pb-0">
<StepperIndicator />
<StepperHeading className="space-y-0.5 text-left">
<StepperTitle>{`Step ${step}`}</StepperTitle>
<StepperDescription>{`Description for step ${step}`}</StepperDescription>
</StepperHeading>
</StepperTrigger>
{step < 3 && (
<StepperSeparator className="absolute inset-y-0 top-[calc(1.5rem+0.125rem)] left-3 -order-1 m-0 -translate-x-1/2 group-data-[orientation=vertical]/stepper:h-[calc(100%-1.5rem-0.25rem)]" />
)}
</StepperItem>
))}
</Stepper>Mixed Elements
import { Avatar, AvatarFallback } from "@tilt-legal/cubitt-components/avatar";
import { Shuffle } from "@tilt-legal/cubitt-icons/ui/outline";
<Stepper defaultValue={2}>
<StepperItem className="not-last:flex-1" step={1}>
<StepperTrigger>
<StepperIndicator>
<Avatar className="size-6">
<AvatarFallback className="text-xs">M</AvatarFallback>
</Avatar>
</StepperIndicator>
</StepperTrigger>
<StepperSeparator />
</StepperItem>
<StepperItem className="not-last:flex-1" loading step={2}>
<StepperTrigger>
<StepperIndicator />
</StepperTrigger>
<StepperSeparator />
</StepperItem>
<StepperItem className="not-last:flex-1" step={3}>
<StepperTrigger>
<StepperIndicator>
<Shuffle aria-hidden="true" className="size-3.5" />
<span className="sr-only">Shuffle</span>
</StepperIndicator>
</StepperTrigger>
</StepperItem>
</Stepper>Progress Bar
<Stepper className="items-start gap-4" defaultValue={2}>
{[1, 2, 3, 4].map((step) => (
<StepperItem className="flex-1" key={step} step={step}>
<StepperTrigger className="w-full flex-col items-start gap-2 rounded">
<StepperIndicator className="h-1 w-full">
<span className="sr-only">{step}</span>
</StepperIndicator>
<StepperHeading className="space-y-0.5">
<StepperTitle>{`Step ${step}`}</StepperTitle>
</StepperHeading>
</StepperTrigger>
</StepperItem>
))}
</Stepper>URL State
<Stepper className="items-start gap-4" defaultValue={1} paramName="step">
{[
{ step: 1, title: "Account", description: "Create your account" },
{ step: 2, title: "Profile", description: "Setup your profile" },
{ step: 3, title: "Settings", description: "Configure settings" },
].map(({ step, title, description }) => (
<StepperItem className="flex-1" key={step} step={step}>
<StepperTrigger className="w-full flex-col items-start gap-2 rounded">
<StepperIndicator className="h-1 w-full">
<span className="sr-only">{step}</span>
</StepperIndicator>
<StepperHeading className="flex flex-col items-start gap-1">
<StepperTitle>{title}</StepperTitle>
<StepperDescription className="text-xs">
{description}
</StepperDescription>
</StepperHeading>
</StepperTrigger>
</StepperItem>
))}
</Stepper>Animated Content
const [showContent, setShowContent] = useState(false);
<>
<PreviewFooter>
<Button onClick={() => setShowContent(!showContent)} variant="secondary">
{showContent ? "Hide" : "Show"} Step Content
</Button>
</PreviewFooter>
<Stepper animateContent className="w-48" defaultValue={2} orientation="vertical">
{[1, 2, 3].map((step) => (
<StepperItem
className="relative not-last:flex-1 items-start pb-12 last:pb-0"
key={step}
step={step}
>
<StepperTrigger className="rounded">
<StepperIndicator />
<StepperHeading className="space-y-0.5 text-left">
<StepperTitle>{step === 2 ? "Review Details" : `Step ${step}`}</StepperTitle>
<StepperDescription>Verify information</StepperDescription>
</StepperHeading>
</StepperTrigger>
{step === 2 && showContent && (
<div className="mt-4 ml-12 w-72 space-y-3 text-base text-fg-2">
<p>Review the uploaded materials before continuing.</p>
<p>The step body can hold supporting copy, form fields, or actions.</p>
<p>When hidden, the layout collapses around the stepper controls.</p>
</div>
)}
{step < 3 && (
<StepperSeparator className="absolute inset-y-0 top-[calc(1.5rem+0.125rem)] left-3 -order-1 m-0 -translate-x-1/2 group-data-[orientation=vertical]/stepper:h-[calc(100%-1.5rem-0.25rem)]" />
)}
</StepperItem>
))}
</Stepper>
</>;API Reference
Stepper
The root component owns active-step state and orientation. It does not add a surface of its own.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultValue | number | 0 | The initial active step when uncontrolled. |
value | number | - | Controlled active step value. |
onValueChange | (value: number) => void | - | Called when the active step changes. |
orientation | "horizontal" | "vertical" | "horizontal" | Stepper layout direction. |
animateContent | boolean | false | Animates non-trigger StepperItem content in and out. |
className | string | - | Additional classes for layout and sizing. |
...props | React.HTMLAttributes<HTMLDivElement> | - | Standard div attributes. |
URL State Props
| Prop | Type | Default | Description |
|---|---|---|---|
paramName | string | - | URL parameter name for syncing the step. |
paramValue | number | - | Controlled value for URL state. |
onUrlValueChange | (value: number | null) => void | - | Called when the URL value changes. |
paramClearOnDefault | boolean | true | Removes the URL parameter when the value matches the default. |
paramThrottle | number | - | Throttle window for URL updates. |
paramDebounce | number | - | Debounce window for URL updates. |
StepperItem
Container for one step. It provides selection state and status to its trigger, indicator, heading, title, description, and separator.
| Prop | Type | Default | Description |
|---|---|---|---|
step | number | - | Step value for this item. |
completed | boolean | false | Sets the item status to completed. This can combine with inactive or active selection state. |
disabled | boolean | false | Disables the step trigger. |
loading | boolean | false | Sets the item status to loading. This can combine with inactive or active selection state. |
className | string | - | Additional classes for the item. |
...props | React.HTMLAttributes<HTMLDivElement> | - | Standard div attributes. |
StepperTrigger
Interactive control that activates its step.
| Prop | Type | Default | Description |
|---|---|---|---|
render | ReactElement | - | Custom element to render as the trigger. |
className | string | - | Additional classes for trigger layout. |
...props | React.ButtonHTMLAttributes<HTMLButtonElement> | - | Standard button attributes. |
StepperIndicator
Visual step marker. Unreached indicators are surface-aware; reached indicators use the brand fill. Status controls whether the marker shows the step number, check icon, or spinner.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Custom indicator content. Defaults to step number, check icon, or spinner. |
className | string | - | Additional classes for shape and size. |
...props | React.HTMLAttributes<HTMLSpanElement> | - | Standard span attributes. |
StepperHeading
Wrapper for step title and description content. It receives the shared hover background from the step trigger and the active selection background when its step is active.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional classes for layout, alignment, and spacing. |
...props | React.HTMLAttributes<HTMLDivElement> | - | Standard div attributes. |
StepperTitle
Title text for the step. Use it inside StepperHeading when title and
description should share one interaction background.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional classes for the title. |
...props | React.HTMLAttributes<HTMLHeadingElement> | - | Standard heading attributes. |
StepperDescription
Supporting text for the step. Use it inside StepperHeading with
StepperTitle.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional classes for the description. |
...props | React.HTMLAttributes<HTMLParagraphElement> | - | Standard paragraph attributes. |
StepperSeparator
Connector between steps. Separators for previous steps use the brand fill; pending separators use the contextual border color one level above the parent surface.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional classes for position, length, and thickness. |
...props | React.HTMLAttributes<HTMLDivElement> | - | Standard div attributes. |