Stepper
A component that guides users through a multi-step process with visual progress indication.
Overview
The Stepper component provides a visual representation of a multi-step process, allowing users to navigate through sequential steps with clear progress indication. It supports both horizontal and vertical orientations, controlled and uncontrolled states, and includes loading states for asynchronous operations.
Usage
import {
Stepper,
StepperIndicator,
StepperItem,
StepperTitle,
StepperTrigger,
StepperDescription,
StepperSeparator,
} from "@tilt-legal/cubitt-components/primitives";<Stepper defaultValue={1}>
<StepperItem step={1}>
<StepperTrigger>
<StepperIndicator />
<StepperTitle>Step One</StepperTitle>
</StepperTrigger>
</StepperItem>
<StepperItem step={2}>
<StepperTrigger>
<StepperIndicator />
<StepperTitle>Step Two</StepperTitle>
</StepperTrigger>
</StepperItem>
</Stepper>Using Cubitt's URL-state hooks, you can sync the stepper state with the URL by providing a paramName:
// Current step synced with ?step=2
<Stepper paramName="step" defaultValue={1}>
<StepperItem step={1}>
<StepperTrigger>
<StepperIndicator />
<StepperTitle>Account Setup</StepperTitle>
</StepperTrigger>
</StepperItem>
<StepperItem step={2}>
<StepperTrigger>
<StepperIndicator />
<StepperTitle>Profile Info</StepperTitle>
</StepperTrigger>
</StepperItem>
<StepperItem step={3}>
<StepperTrigger>
<StepperIndicator />
<StepperTitle>Complete</StepperTitle>
</StepperTrigger>
</StepperItem>
</Stepper>
// Advanced options:
<Stepper
paramName="wizard"
paramClearOnDefault={true} // Remove param when value equals default
paramDebounce={300} // Debounce URL updates
onUrlValueChange={(step) => console.log('Current step:', step)}
defaultValue={1}
>
{/* ... */}
</Stepper>Examples
Basic Stepper
A horizontal stepper with titles and descriptions.
<Stepper defaultValue={2}>
{[
{
step: 1,
title: "Step One",
description: "Desc for step one",
},
{
step: 2,
title: "Step Two",
description: "Desc for step two",
},
{
step: 3,
title: "Step Three",
description: "Desc for step three",
},
].map(({ step, title, description }) => (
<StepperItem key={step} step={step} className="relative flex-1 flex-col!">
<StepperTrigger className="flex-col gap-3 rounded">
<StepperIndicator />
<div className="space-y-0.5 px-2">
<StepperTitle>{title}</StepperTitle>
<StepperDescription className="max-sm:hidden">
{description}
</StepperDescription>
</div>
</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 Orientation
Stepper with vertical orientation for more detailed descriptions.
<Stepper defaultValue={2} orientation="vertical">
{[
{
step: 1,
title: "Step One",
description: "Desc for step one",
},
{
step: 2,
title: "Step Two",
description: "Desc for step two",
},
{
step: 3,
title: "Step Three",
description: "Desc for step three",
},
].map(({ step, title, description }) => (
<StepperItem
key={step}
step={step}
className="relative items-start not-last:flex-1"
>
<StepperTrigger className="items-start rounded pb-12 last:pb-0">
<StepperIndicator />
<div className="mt-0.5 space-y-0.5 px-2 text-left">
<StepperTitle>{title}</StepperTitle>
<StepperDescription>{description}</StepperDescription>
</div>
</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=horizontal]/stepper:w-[calc(100%-1.5rem-0.25rem)] group-data-[orientation=horizontal]/stepper:flex-none group-data-[orientation=vertical]/stepper:h-[calc(100%-1.5rem-0.25rem)]" />
)}
</StepperItem>
))}
</Stepper>Mixed Elements
Stepper with custom indicators including images, loading states, and icons.
import { Shuffle } from "@tilt-legal/cubitt-icons/ui/outline";
<Stepper defaultValue={2}>
<StepperItem step={1} className="not-last:flex-1">
<StepperTrigger>
<StepperIndicator>
<img
className="rounded-full object-cover aspect-square"
src="https://tilt.legal/_next/image?url=%2Fapi%2Fmedia%2Ffile%2Fsandon-lai.jpg&w=3840&q=75"
alt="Mike Palmer"
/>
</StepperIndicator>
</StepperTrigger>
<StepperSeparator />
</StepperItem>
<StepperItem step={2} className="not-last:flex-1" loading>
<StepperTrigger>
<StepperIndicator />
</StepperTrigger>
<StepperSeparator />
</StepperItem>
<StepperItem step={3} className="not-last:flex-1">
<StepperTrigger>
<StepperIndicator>
<Shuffle size={14} aria-hidden="true" />
<span className="sr-only">Shuffle</span>
</StepperIndicator>
</StepperTrigger>
</StepperItem>
</Stepper>;Progress Bar Style
Stepper styled as a progress bar.
<Stepper defaultValue={2} className="items-start gap-4">
{[
{ step: 1, title: "Step One" },
{ step: 2, title: "Step Two" },
{ step: 3, title: "Step Three" },
{ step: 4, title: "Step Four" },
].map(({ step, title }) => (
<StepperItem key={step} step={step} className="flex-1">
<StepperTrigger className="w-full flex-col items-start gap-2 rounded">
<StepperIndicator className="bg-border h-1 w-full">
<span className="sr-only">{step}</span>
</StepperIndicator>
<div className="space-y-0.5">
<StepperTitle>{title}</StepperTitle>
</div>
</StepperTrigger>
</StepperItem>
))}
</Stepper>URL State
Stepper with URL state synchronization for shareable progress.
// The current step will be synced with ?step=2
<Stepper paramName="step" defaultValue={1} className="items-start gap-4">
{[
{ 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 key={step} step={step} className="flex-1">
<StepperTrigger className="w-full flex-col items-start gap-2 rounded">
<StepperIndicator className="bg-border h-1 w-full" />
<div className="flex flex-col items-start gap-1">
<StepperTitle>{title}</StepperTitle>
<StepperDescription className="text-xs">
{description}
</StepperDescription>
</div>
</StepperTrigger>
</StepperItem>
))}
</Stepper>Animated Content
Stepper with smooth height transitions when content is added or removed. Enable with animateContent prop.
const [showContent, setShowContent] = useState(false);
<Stepper
animateContent
className="w-full"
defaultValue={2}
orientation="vertical"
>
{[
{
step: 1,
title: "Upload Documents",
description: "Select files to upload",
},
{ step: 2, title: "Review Details", description: "Verify information" },
{ step: 3, title: "Complete", description: "Finish submission" },
].map(({ step, title, description }) => (
<StepperItem
key={step}
step={step}
className="relative items-start not-last:flex-1"
>
<StepperTrigger className="items-start rounded pb-4">
<StepperIndicator />
<div className="mt-0.5 space-y-0.5 px-2 text-left">
<StepperTitle>{title}</StepperTitle>
<StepperDescription>{description}</StepperDescription>
</div>
</StepperTrigger>
{/* Content outside StepperTrigger animates in/out */}
{step === 2 && showContent && (
<div className="ml-8 rounded-md border bg-muted/50 p-3">
<p className="text-sm text-muted-foreground">
This content animates in and out smoothly when toggled.
</p>
</div>
)}
{step < 3 && <StepperSeparator className="..." />}
</StepperItem>
))}
</Stepper>;API Reference
Stepper
Root component that manages stepper state.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultValue | number | 0 | The default active step when uncontrolled. |
value | number | - | The controlled active step value. |
onValueChange | (value: number) => void | - | Event handler called when the active step changes. |
orientation | 'horizontal' | 'vertical' | 'horizontal' | The orientation of the stepper. |
animateContent | boolean | false | When true, content within StepperItem animates with smooth height transitions. |
className | string | - | Additional CSS classes to apply to the stepper. |
URL State Props
The following props enable URL state management by syncing the stepper step with URL parameters via TanStack Router search params. When paramName is provided, the stepper's state will be reflected in the URL.
| Prop | Type | Default | Description |
|---|---|---|---|
paramName | string | - | URL parameter name for syncing state with URL. When provided, enables URL state management. |
paramValue | number | - | Controlled value for the URL parameter (syncs with stepper value). |
onUrlValueChange | (value: number | null) => void | - | Callback when URL parameter value changes. |
paramClearOnDefault | boolean | true | Remove URL param when value equals default. |
paramThrottle | number | - | Throttle URL updates in milliseconds. |
paramDebounce | number | - | Debounce URL updates in milliseconds. |
StepperItem
Container for individual step content.
| Prop | Type | Default | Description |
|---|---|---|---|
step | number | - | The step number for this item (required). |
completed | boolean | false | Whether this step is completed. |
disabled | boolean | false | Whether this step is disabled. |
loading | boolean | false | Whether this step is in a loading state. |
className | string | - | Additional CSS classes for the item. |
StepperTrigger
Button that activates the step.
| Prop | Type | Default | Description |
|---|---|---|---|
render | ReactElement | - | Custom element to render as the trigger |
className | string | - | Additional CSS classes for the trigger |
StepperIndicator
Visual indicator for the step (number, icon, or custom element).
| Prop | Type | Default | Description |
|---|---|---|---|
children | node | - | Custom content to render in the indicator |
className | string | - | Additional CSS classes for the indicator |
StepperTitle
The title text for the step.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes for the title. |
StepperDescription
Description text that provides context about the step.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes for the description. |
StepperSeparator
Visual separator between steps.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes for the separator. |