Stepper
Accessible and typesafe Stepper component to create step-by-step workflows.
Content for step-1
About
Stepperize is built and maintained by damianricobelli.
Installation
pnpm dlx shadcn@latest add stepper
Structure
A Stepper
component is composed of the following parts:
StepperProvider
- Handles the stepper logic.StepperNavigation
- Contains the buttons and labels to navigate through the steps.StepperStep
- Step component.StepperTitle
- Step title.StepperDescription
- Step description.StepperPanel
- Section to render the step content based on the current step.StepperControls
- Section to render the buttons to navigate through the steps.
Usage
import { defineStepper } from "@/components/ui/stepper"
const {
StepperProvider,
StepperControls,
StepperDescription,
StepperNavigation,
StepperPanel,
StepperStep,
StepperTitle,
} = defineStepper(
{ id: "step-1", title: "Step 1" },
{ id: "step-2", title: "Step 2" },
{ id: "step-3", title: "Step 3" }
)
export function Component() {
return (
<StepperProvider>
<StepperNavigation>
<StepperStep>
<StepperTitle />
<StepperDescription />
</StepperStep>
...
</StepperNavigation>
<StepperPanel />
<StepperControls>...</StepperControls>
</StepperProvider>
)
}
Your first Stepper
Let's start with the most basic stepper. A stepper with a horizontal navigation.
Create a stepper instance with the defineStepper
function.
const { StepperProvider, ... } = defineStepper(
{ id: "step-1", title: "Step 1" },
{ id: "step-2", title: "Step 2" },
{ id: "step-3", title: "Step 3" }
)
Wrap your application in a StepperProvider
component.
export function MyFirstStepper() {
return <StepperProvider>...</StepperProvider>
}
Add a StepperNavigation
component to render the navigation buttons and
labels.
const { StepperProvider, StepperNavigation, StepperStep, StepperTitle } =
defineStepper(
{ id: "step-1", title: "Step 1" },
{ id: "step-2", title: "Step 2" },
{ id: "step-3", title: "Step 3" }
)
export function MyFirstStepper() {
return (
<StepperProvider>
{({ methods }) => (
<StepperNavigation>
{methods.all.map((step) => (
<StepperStep of={step.id} onClick={() => methods.goTo(step.id)}>
<StepperTitle>{step.title}</StepperTitle>
</StepperStep>
))}
</StepperNavigation>
)}
</StepperProvider>
)
}
Add a StepperPanel
component to render the content of the step.
const { StepperProvider, StepperPanel, ... } = defineStepper(
{ id: "step-1", title: "Step 1" },
{ id: "step-2", title: "Step 2" },
{ id: "step-3", title: "Step 3" }
)
export function MyFirstStepper() {
return (
<StepperProvider>
{({ methods }) => (
<>
{/* StepperNavigation code */}
{methods.switch({
"step-1": (step) => <StepperPanel />,
"step-2": (step) => <StepperPanel />,
"step-3": (step) => <StepperPanel />,
})}
</>
)}
</StepperProvider>
)
}
Add a StepperControls
component to render the buttons to navigate through
the steps.
const { StepperProvider, StepperControls, ... } = defineStepper(
{ id: "step-1", title: "Step 1" },
{ id: "step-2", title: "Step 2" },
{ id: "step-3", title: "Step 3" }
)
export function MyFirstStepper() {
return (
<StepperProvider>
{({ methods }) => (
<>
{/* StepperNavigation code */}
{/* StepperPanel code */}
<StepperControls>
{!methods.isLast && (
<Button
variant="secondary"
onClick={methods.prev}
disabled={methods.isFirst}
>
Previous
</Button>
)}
<Button onClick={methods.isLast ? methods.reset : methods.next}>
{methods.isLast ? "Reset" : "Next"}
</Button>
</StepperControls>
</>
)}
</StepperProvider>
)
}
Add some styles to make it look nice.
const {
StepperProvider,
StepperNavigation,
StepperStep,
StepperTitle,
StepperControls,
StepperPanel,
} = defineStepper(
{ id: "step-1", title: "Step 1" },
{ id: "step-2", title: "Step 2" },
{ id: "step-3", title: "Step 3" }
)
export function MyFirstStepper() {
return (
<StepperProvider className="space-y-4">
{({ methods }) => (
<>
<StepperNavigation>
{methods.all.map((step) => (
<StepperStep of={step} onClick={() => methods.goTo(step.id)}>
<StepperTitle>{step.title}</StepperTitle>
</StepperStep>
))}
</StepperNavigation>
{methods.switch({
"step-1": (step) => <Content id={step.id} />,
"step-2": (step) => <Content id={step.id} />,
"step-3": (step) => <Content id={step.id} />,
})}
<StepperControls>
{!methods.isLast && (
<Button
variant="secondary"
onClick={methods.prev}
disabled={methods.isFirst}
>
Previous
</Button>
)}
<Button onClick={methods.isLast ? methods.reset : methods.next}>
{methods.isLast ? "Reset" : "Next"}
</Button>
</StepperControls>
</>
)}
</StepperProvider>
)
}
const Content = ({ id }: { id: string }) => {
return (
<StepperPanel className="h-[200px] content-center rounded border bg-slate-50 p-8">
<p className="text-xl font-normal">Content for {id}</p>
</StepperPanel>
)
}
Components
The components in stepper.tsx
are built to be composable i.e you build your stepper by putting the provided components together.
They also compose well with other shadcn/ui components such as DropdownMenu, Collapsible or Dialog etc.
If you need to change the code in stepper.tsx
, you are encouraged to do so. The code is yours. Use stepper.tsx
as a starting point and build your own.
In the next sections, we'll go over each component and how to use them.
If you want to use
@stepperize/react API directly,
like when
, switch
, match
, etc. you can use the useStepper
hook from
your stepper instance and build your own components.
defineStepper
The defineStepper
function is used to define the steps. It returns a Stepper
instance with a hook and utils to interact with the stepper.
Unlike @stepperize/react
, defineStepper
also offers all the components for rendering the stepper.
For example, you can define the steps like this:
const stepperInstance = defineStepper(
{ id: "step-1", title: "Step 1", description: "Step 1 description" },
{ id: "step-2", title: "Step 2", description: "Step 2 description" },
{ id: "step-3", title: "Step 3", description: "Step 3 description" }
)
Each instance will return:
steps
- Array of steps.useStepper
- Hook to interact with the stepper component.utils
- Provides a set of pure functions for working with steps.
And the components:
StepperProvider
StepperNavigation
StepperStep
StepperTitle
StepperDescription
StepperPanel
StepperControls
Each step in the defineStepper
needs only an id
to work and they are not
limited to any type. You can define anything within each step, even
components!
useStepper
The useStepper
hook is used to interact with the stepper. It provides methods to interact with and render your stepper.
StepperProvider
The StepperProvider
component is used to provide the stepper instance from defineStepper
to the other components. You should always wrap your application in a StepperProvider
component.
Allow us to work with the useStepper
hook in components that are within the provider.
For example:
const { StepperProvider, useStepper } = defineStepper(
{ id: "step-1", title: "Step 1" },
{ id: "step-2", title: "Step 2" },
{ id: "step-3", title: "Step 3" }
)
export function MyStepper() {
const methods = useStepper() // ❌ This won't work if the component is not within the provider
return (
<StepperProvider>
<MyCustomComponent />
</StepperProvider>
)
}
function MyCustomComponent() {
const methods = useStepper() // ✅ This will work
return <div>{methods.currentStep.title}</div>
}
You also get access to the methods in the children's component
export function MyStepper() {
return (
<StepperProvider>
{({ methods }) => (
...
)}
</StepperProvider>
)
}
You can set the initial step and metadata for the stepper passing these props:
initialStep
- The ID of the initial step to displayinitialMetadata
- The initial metadata to set for the steps. See Metadata for more information.
If you don't need the methods
prop, you can just pass the children directly
and get the methods from the useStepper
hook from your stepper instance.
Props
Name | Type | Description |
---|---|---|
variant | horizontal, vertical or circle | Style of the stepper. |
labelOrientation | horizontal, vertical | Orientation of the labels. This is only applicable if variant is "horizontal" . |
tracking | boolean | Track the scroll position of the stepper. |
initialStep | string | Initial step to render. |
initialMetadata | Record<string, any> | Initial metadata. |
StepperNavigation
The StepperNavigation
component is used to render the navigation buttons and labels.
StepperStep
The StepperStep
component is a wrapper of the button and labels. You just need to pass the of
prop which is the step id you want to render.
This is a good place to add your onClick
handler.
Props
Name | Type | Description |
---|---|---|
of | string | Step to render. |
icon | React.ReactNode | Icon to render instead of the step number |
To keep the stepper simple and consistent, StepperStep
only accepts these 3
types of children: StepperTitle
, StepperDescription
and StepperPanel
StepperTitle
The StepperTitle
component is used to render the title of the step.
Props
Name | Type | Description |
---|---|---|
children | React.ReactNode | Title to render. |
asChild | boolean | Render as child. |
StepperDescription
The StepperDescription
component is used to render the description of the step.
Props
Name | Type | Description |
---|---|---|
children | React.ReactNode | Description to render. |
asChild | boolean | Render as child. |
StepperPanel
The StepperPanel
component is used to render the content of the step.
Props
Name | Type | Description |
---|---|---|
children | React.ReactNode | Content to render. |
asChild | boolean | Render as child. |
StepperControls
The StepperControls
component is used to render the buttons to navigate through the steps.
Props
Name | Type | Description |
---|---|---|
children | React.ReactNode | Buttons to render. |
asChild | boolean | Render as child. |
Before/after actions
You can add a callback to the next
and prev
methods to execute a callback before or after the action is executed.
This is useful if you need to validate the form or check if the step is valid before moving to the prev/next step.
For example:
methods.beforeNext(async () => {
const valid = await form.trigger()
if (!valid) return false
return true
})
That function will validate the form and check if the step is valid before moving to the next step returning a boolean value.
More info about the beforeNext
and beforePrev
methods can be found in the API References.
Skip steps
Through the methods you can access functions like goTo
to skip to a specific step.
// From step 1 to step 3
methods.goTo("step-3")
Metadata
You can add metadata to each step to store any information you need. This data can be accessed in the useStepper
hook and changed at any time.
const { metadata, getMetadata, setMetadata, resetMetadata } = useStepper()
Multi Scoped
The StepperProvider
component can be used multiple times in the same application. Each instance will be independent from the others.
const stepperInstance1 = defineStepper(
{ id: "step-1", title: "Step 1" },
{ id: "step-2", title: "Step 2" },
{ id: "step-3", title: "Step 3" }
)
const stepperInstance2 = defineStepper(
{ id: "step-1", title: "Step 1" },
{ id: "step-2", title: "Step 2" },
{ id: "step-3", title: "Step 3" }
)
<stepperInstance1.StepperProvider>
<stepperInstance2.StepperProvider>
...
</stepperInstance2.StepperProvider>
</stepperInstance1.StepperProvider>
Examples
Variants
Content for step-1
Responsive variant
If you need to render the stepper in a responsive way, you can use a custom hook to detect the screen size and render the stepper in a different variant.
Resize the window to see the stepper in a different variant.
Content for step-1
Description
You can add a description to each step by using <StepperDescription />
component inside <StepperStep />
.
Content for step-1
Label Orientation
You can change the orientation of the labels by using the labelOrientation
prop in the Stepper
component.
This is only applicable if variant
is "horizontal"
.
Content for step-1
Icon
You can add an icon to each step by using the icon
prop in the StepperStep
component.
Content for step-1
Step tracking
If you need to track the scroll position of the stepper, you can use the tracking
prop in the Stepper
component.
Forms
Custom components
If you need to add custom components, you can do so by using the useStepper
hook and utils
from your stepper instance.
If all this is not enough for you, you can use the @stepperize/react API to create your own stepper.