Basic Usage
The stepper can be used in two directions: horizontal and vertical.
Each direction has its own unique default design in order to support different use cases.
Direction: Horizontal
function StepperHorizontalBasicExample() {
const steps = [
{ id: '1', label: 'Account Details' },
{ id: '2', label: 'Personal Information' },
{ id: '3', label: 'Payment Method' },
{ id: '4', label: 'Review & Submit' },
];
const [stepperState, stepperApi] = useStepper({ steps });
const [complete, setComplete] = useState(false);
const handleNext = () => {
if (stepperState.activeStepId === '4') {
setComplete(true);
} else {
stepperApi.goNextStep();
}
};
const handlePrevious = () => {
setComplete(false);
stepperApi.goPreviousStep();
};
const handleReset = () => {
setComplete(false);
stepperApi.reset();
};
return (
<VStack gap={2}>
<Stepper
direction="horizontal"
activeStepId={stepperState.activeStepId}
steps={steps}
complete={complete}
/>
<HStack gap={1}>
<Button
variant="secondary"
onClick={handlePrevious}
disabled={stepperState.activeStepId === '1'}
>
Previous
</Button>
<Button onClick={handleNext} disabled={complete}>
{stepperState.activeStepId === '4' ? 'Complete' : 'Next'}
</Button>
{complete && <Button onClick={handleReset}>Reset</Button>}
</HStack>
</VStack>
);
}
Direction: Vertical
function StepperVerticalBasicExample() {
const steps = [
{ id: '1', label: 'Account Details' },
{ id: '2', label: 'Personal Information' },
{ id: '3', label: 'Payment Method' },
{ id: '4', label: 'Review & Submit' },
];
const [stepperState, stepperApi] = useStepper({ steps });
const [complete, setComplete] = useState(false);
const handleNext = () => {
if (stepperState.activeStepId === '4') {
setComplete(true);
} else {
stepperApi.goNextStep();
}
};
const handlePrevious = () => {
setComplete(false);
stepperApi.goPreviousStep();
};
const handleReset = () => {
setComplete(false);
stepperApi.reset();
};
return (
<VStack gap={2}>
<Stepper
direction="vertical"
activeStepId={stepperState.activeStepId}
steps={steps}
complete={complete}
/>
<HStack gap={1}>
<Button
variant="secondary"
onClick={handlePrevious}
disabled={stepperState.activeStepId === '1'}
>
Previous
</Button>
<Button onClick={handleNext} disabled={complete}>
{stepperState.activeStepId === '4' ? 'Complete' : 'Next'}
</Button>
{complete && <Button onClick={handleReset}>Reset</Button>}
</HStack>
</VStack>
);
}
Step Config
The Stepper is ultimately a visual representation of an array of step objects (i.e. StepperValue[]
).
These should be defined outside of the component or memoized prior to rendering a Stepper.
Commonly used or required StepperValue properties:
id
- A required, unique identifier for the step.
label
- The label of the step. Optionally exclude this property to hide the label text.
subSteps
- An optional array of sub-steps to nest under the step.
metadata
- An optional object that can be used to store additional data about the step. This is useful when providing your own custom Step components.
useStepper hook
Call the useStepper
hook to initialize stepper state, access the current state and perform state mutations with its API.
The hook returns a tuple where the first member is the current stepper state containing the activeStepId
.
const [stepperState, stepperApi] = useStepper({ steps });
<Stepper direction="horizontal" activeStepId={stepperState.activeStepId} steps={steps} />;
The second member is an API for manipulating the stepper state and includes the following methods:
type StepperApi = {
goToStep: (id: string) => void;
goNextStep: () => void;
goPreviousStep: () => void;
reset: () => void;
};
Common Patterns & Use Cases
Sub-steps
A common use-case for the vertical stepper is to visualize long and complex workflows with nested/grouped steps.
A StepperValue object optionally accepts a subSteps
property, which is also an array of StepperValue objects.
Avoid Deep NestingSteps can be nested arbritrarily deep, however CDS does not advise nesting deeper than one level.
function StepperVerticalSubStepsExample() {
const steps: StepperValue[] = [
{
id: 'first-step',
label: 'First step',
},
{
id: 'second-step',
label: 'Second step',
subSteps: [
{
id: 'second-step-substep-one',
label: 'Substep one',
},
{
id: 'second-step-substep-two',
label: 'Substep two',
},
{
id: 'second-step-substep-three',
label: 'Substep three',
},
],
},
{
id: 'final-step',
label: 'Final step',
},
];
const [stepperState, stepperApi] = useStepper({ steps });
const [complete, setComplete] = useState(false);
const handleNext = () => {
if (stepperState.activeStepId === 'final-step') {
setComplete(true);
} else {
stepperApi.goNextStep();
}
};
const handlePrevious = () => {
setComplete(false);
stepperApi.goPreviousStep();
};
const handleReset = () => {
setComplete(false);
stepperApi.reset();
};
return (
<VStack gap={2}>
<Stepper
direction="vertical"
activeStepId={stepperState.activeStepId}
steps={steps}
complete={complete}
accessibilityLabel="Example Stepper"
/>
<HStack gap={1}>
<Button
variant="secondary"
onClick={handlePrevious}
disabled={stepperState.activeStepId === 'first-step'}
>
Previous
</Button>
<Button onClick={handleNext} disabled={complete}>
{stepperState.activeStepId === 'final-step' ? 'Complete' : 'Next'}
</Button>
{complete && <Button onClick={handleReset}>Reset</Button>}
</HStack>
</VStack>
);
}
Customization Options
1. Custom Components
Stepper is highly customizable. Use the Component props to customize Stepper with your own React components.
Components can be set on the Stepper or individually on each step. Custom components set on a specific step will override the same component set on the Stepper.
Customizable subcomponents
- StepperHeaderComponent: Rendered above the entire stepper, helpful to display an overall title or label.
- StepperIconComponent: Useful for showing a small visual with the step to convey state or the intent of the step.
- StepperProgressComponent: Can be used to show an animated marker of progress with each step.
- StepperLabelComponent: A component responsible for rendering the label text or main content associated with the step.
- StepperSubstepContainerComponent: Responsible for rendering the recursive sub-steps of a step.
Below are some basic diagrams to help visualize the Stepper anatomy in its two orientations.
direction: horizontal
┌─────────────────────────────────┐
│ Step (VStack) │
│ ┌───────────────────────────┐ │
│ │ HStack │ │
│ │ ┌──────┐ ┌──────────────┐ │ │
│ │ │ Icon │ │ Progress │ │ │
│ │ │ │ │ │ │ │
│ │ └──────┘ └──────────────┘ │ │
│ └───────────────────────────┘ │
│ ┌───────────────────────────┐ │
│ │ Label │ │
│ └───────────────────────────┘ │
│ ┌───────────────────────────┐ │
│ │ SubstepContainer │ │
│ │ (recursive) │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
direction: vertical
┌─────────────────────────────────────────────────┐
│ Step (VStack) │
│ ┌───────────────────────────────────────────┐ │
│ │ HStack │ │
│ │ ┌─────────────┐ ┌───────────────────┐ │ │
│ │ │ VStack │ │ VStack │ │ │
│ │ │ ┌─────────┐ │ │ ┌───────────────┐ │ │ │
│ │ │ │ Icon │ │ │ │ Label │ │ │ │
│ │ │ └─────────┘ │ │ └───────────────┘ │ │ │
│ │ │ ┌─────────┐ │ │ ┌───────────────┐ │ │ │
│ │ │ │Progress │ │ │ │ Substeps │ │ │ │
│ │ │ └─────────┘ │ │ │ (recursive) │ │ │ │
│ │ └─────────────┘ │ └───────────────┘ │ │ │
│ └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
Each custom component receives a specific set of props that provide access to step data and state.
When writing your own components, make sure you're getting the most out of Stepper by importing our custom component types like so:
import type { StepperLabelProps } from '@coinbase/cds-web/stepper/Stepper';
const TravelBookingLabel = ({
step,
active,
depth,
}: StepperLabelProps) => {
const { label, metadata, id } = step;
const subtitle = metadata?.subtitle as string;
if (depth === 0 && metadata) {
return (
<ListCell
description={metadata.name as string}
detail={metadata.date as string}
innerSpacing={{ paddingTop: 0, paddingBottom: 0 }}
maxWidth={350}
minHeight={undefined}
outerSpacing={{ paddingTop: 0, paddingBottom: 3, paddingStart: 0, paddingEnd: 0 }}
subdetail={metadata.time as string}
title={label}
/>
);
}
Using Default ComponentsIn many cases, it may be helpful to compose the default Stepper Components within your own. For example, you may want to make the default label pressable.
All of the default components used by Stepper are exported and available for you to use.
import { DefaultStepperLabelHorizontal } from '@coinbase/cds-web/stepper';
<Stepper
direction="horizontal"
StepperLabelComponent={(props) => (
<Pressable onClick={handleStepClick}>
<DefaultStepperLabelHorizontal {...props} />
</Pressable>
)}
/>;
Null Components
Pass null to any of the component props to completely disable its functionality and hide it from the user.
function StepperNullComponentsExample() {
const steps = [
{ id: '1', label: 'Account Details' },
{ id: '2', label: 'Personal Information' },
{ id: '3', label: 'Payment Method' },
{ id: '4', label: 'Review & Submit' },
];
const [stepperState, stepperApi] = useStepper({ steps });
const [complete, setComplete] = useState(false);
const handleNext = () => {
if (stepperState.activeStepId === '4') {
setComplete(true);
} else {
stepperApi.goNextStep();
}
};
const handlePrevious = () => {
setComplete(false);
stepperApi.goPreviousStep();
};
return (
<VStack gap={2}>
<Stepper
direction="horizontal"
StepperLabelComponent={null}
activeStepId={stepperState.activeStepId}
steps={steps}
complete={complete}
/>
<HStack gap={1}>
<Button
variant="secondary"
onClick={handlePrevious}
disabled={stepperState.activeStepId === '1'}
>
Previous
</Button>
<Button onClick={handleNext} disabled={complete}>
{stepperState.activeStepId === '4' ? 'Complete' : 'Next'}
</Button>
</HStack>
</VStack>
);
}
The metadata
property on each step allows you to store additional data that can be used
by custom components to create rich, interactive experiences. This is particularly useful
for complex workflows where each step needs to display contextual information.
function StepperCustomMetadataExample() {
const CustomBookingLabel = ({ step, active }) => {
const { label, metadata } = step;
return (
<ListCell
title={label}
description={metadata.name}
detail={metadata.date}
subdetail={metadata.time}
maxWidth={350}
innerSpacing={{ paddingTop: 0, paddingBottom: 0 }}
outerSpacing={{ paddingTop: 0, paddingBottom: 3, paddingStart: 0, paddingEnd: 0 }}
minHeight={undefined}
/>
);
};
const bookingSteps: StepperValue<{
name: string;
date: string;
time: string;
}>[] = [
{
id: 'book-hotel',
label: 'Book Hotel',
metadata: {
name: 'Marriott Downtown',
date: '2025-06-13',
time: '3:00 PM Check-in',
},
},
{
id: 'book-flight',
label: 'Book Flight',
metadata: {
name: 'Delta Airlines',
date: '2025-06-13',
time: '11:03 AM Departure',
},
},
{
id: 'rental-car',
label: 'Reserve Rental Car',
metadata: {
name: 'Enterprise Rent-A-Car',
date: '2025-06-14',
time: '2:24 PM Pickup',
},
},
];
const [stepperState, stepperApi] = useStepper({
steps: bookingSteps,
});
return (
<Stepper
direction="vertical"
activeStepId={stepperState.activeStepId}
steps={bookingSteps}
complete={true}
StepperLabelComponent={CustomBookingLabel}
accessibilityLabel="Travel Booking Stepper"
/>
);
}
2. style and className APIs
The Stepper component provides flexible styling options through the classNames
and styles
props.
Through these props, you can apply CSS classes and inline styles to specific subcomponents of the Stepper; the same components which you can override with the Component
props.
classNames
The classNames
prop allows you to apply CSS classes to specific child elements.
It accepts an object with the following optional keys:
header
- Applied to the header component
step
- Applied to each individual step element
substepContainer
- Applied to each substep container element
progress
- Applied to each step progress bar element
label
- Applied to each step label element
icon
- Applied to each step icon element
<Stepper
classNames={{
step: 'custom-step',
label: 'custom-label',
progress: 'custom-marker',
}}
/>
data-attributesEach step element, automatically receives data attributes that reflect its step's current state, making it easy to style different step states with CSS.
data-step-active=(true|false)
- Indicates when the step is the current, active step:
data-step-complete=(true|false)
- Indicates when the stepper has been completed
data-step-visited=(true|false)
- Indicates when the position of the active step is greater than or equal to the step's position in the stepper
data-step-descendent-active=(true|false)
- Indicates when the active step has a descendent sub-step that is active
[data-step-active='true'] {
color: var(--color-fgPrimary);
font-weight: bold;
}
styles
The styles
prop allows you to apply inline styles to specific child elements.
It follows the same structure as classNames
:
<Stepper
direction="horizontal"
styles={{
step: { ... },
substepContainer: { ... },
label: { ... },
progress: { ... },
}}
/>