Skip to content
Drivn
7 min read

React Stepper Component Examples

Copy-paste React Stepper examples: checkout flow, vertical onboarding wizard, icon indicators, no-line pager, disabled steps, and Next/Back controls.

A stepper is the right indicator whenever a flow walks the user through a fixed sequence of stages — a checkout that runs cart, shipping, payment, review; an onboarding wizard that collects an account, a profile, preferences, and a confirmation; a settings migration that asks the user to pick a plan, verify the email, and accept terms. The Stepper in Drivn is a single ~120 line client component with a compound Stepper.Item API, automatic step numbering, a built-in connector line between items, and horizontal and vertical orientations baked in.

Every example on this page imports Stepper from @/components/ui/stepper — the path the Drivn CLI installs it under — and pairs it with React.useState inside a 'use client' component. The parent owns the active step index, each <Stepper.Item /> takes an optional label, icon, or disabled prop, and the parent's onStepChange callback fires when a user clicks an indicator. Items below the current step render a Check icon automatically; the active item gets a ring; upcoming items render the number or label dimmed. Every snippet is TypeScript because Drivn ships TypeScript-only.

The examples below cover a four-step checkout flow with text labels, an icon-led account-creation wizard, a vertical multi-stage settings flow, a compact no-line pager, a disabled gate that blocks forward navigation, and a Next/Back controls pattern that wraps the stepper inside a card.

A checkout flow with text labels

The canonical Stepper use is a checkout indicator with four named stages — cart, shipping, payment, review — sitting above the form that swaps based on the active step. Hold the step in React.useState, pass it as the step prop, and pass setStep as onStepChange so the user can jump back to a completed stage by clicking its indicator. Each Stepper.Item takes a label prop, which widens the indicator with px-3 and renders the text in place of the auto-number.

Drop this above a Tabs panel or a conditional render that swaps a CartForm, ShippingForm, PaymentForm, and ReviewSummary based on step. The completed states light up automatically as the user advances — every indicator below step gets a green Check icon, the active indicator gets the primary ring, and upcoming indicators stay muted. Pair the stepper with a row of Button Next / Back controls under the form.

1"use client"
2
3import * as React from "react"
4import { Stepper } from "@/components/ui/stepper"
5
6export default function CheckoutSteps() {
7 const [step, setStep] = React.useState(1)
8
9 return (
10 <Stepper step={step} onStepChange={setStep}>
11 <Stepper.Item label="Cart" />
12 <Stepper.Item label="Shipping" />
13 <Stepper.Item label="Payment" />
14 <Stepper.Item label="Review" />
15 </Stepper>
16 )
17}

An icon-led account creation wizard

For an onboarding flow where the stages are best telegraphed by an icon rather than a label — a user silhouette for the account step, a document for the profile step, an envelope for the verification step — pass an icon prop on each Stepper.Item instead of a label. The component renders the icon at size-4 inside the round indicator, and the auto-numbering falls away. Once a step is completed the Check icon takes over regardless of the original icon, so the visual treatment of "this stage is done" stays consistent across the row.

Use lucide-react icons (the same set the rest of the Drivn registry pulls from) to keep the bundle lean. The icon prop type is React.ComponentType<{ className?: string }> — pass the component reference (icon={User}), not a JSX element. Wrap the whole stepper inside a Card header and render the matching form below to land the visual hierarchy.

1"use client"
2
3import * as React from "react"
4import { User, FileText, Send, Check as CheckIcon } from "lucide-react"
5import { Stepper } from "@/components/ui/stepper"
6
7export default function OnboardingWizard() {
8 const [step, setStep] = React.useState(0)
9
10 return (
11 <Stepper step={step} onStepChange={setStep}>
12 <Stepper.Item icon={User} />
13 <Stepper.Item icon={FileText} />
14 <Stepper.Item icon={Send} />
15 </Stepper>
16 )
17}

A vertical multi-stage settings flow

When the flow is tall enough that a horizontal row would crowd the viewport — a settings migration with eight stages, an admin onboarding sequence inside a sidebar, a documentation walkthrough on a marketing page — switch to orientation="vertical". The root flips to flex-col, the indicators stack from top to bottom, and the connector becomes a vertical column (w-0.5 min-h-6 self-center) sized to give each stage breathing room.

The same step, onStepChange, label, and icon props apply. Render the active stage's form to the right of the stepper inside a two-column grid — the stepper holds the structural navigation and the right column owns the inputs. For a stepper used purely as a read-only progress indicator, drop the onStepChange prop and the indicators stay clickable but inert. Pair the layout with a Separator between the stepper and the right-hand panel.

1"use client"
2
3import * as React from "react"
4import { Stepper } from "@/components/ui/stepper"
5
6export default function VerticalWizard() {
7 const [step, setStep] = React.useState(2)
8
9 return (
10 <Stepper
11 step={step}
12 orientation="vertical"
13 onStepChange={setStep}
14 >
15 <Stepper.Item label="Account" />
16 <Stepper.Item label="Profile" />
17 <Stepper.Item label="Review" />
18 <Stepper.Item label="Done" />
19 </Stepper>
20 )
21}

A compact no-line pager

For a slide-deck pager, a multi-step photo carousel indicator, or an onboarding pill row in a marketing hero — situations where the connector line between indicators reads as noise rather than progress — pass line={false} on the parent. The root applies the justify-between class so the items spread across the available width with even spacing, and the connector lines drop out of the render entirely. Each indicator still cycles through the active / completed / upcoming states, still auto-numbers, still fires onStepChange on click.

Drop this on a landing page to indicate "you are on slide 2 of 4" or inside a Dialog header to indicate a short three-step confirmation flow. With no labels and no icons, the indicators render as round numbered pills — 1, 2, 3, 4 — which is the smallest possible visual treatment the Stepper supports.

1"use client"
2
3import * as React from "react"
4import { Stepper } from "@/components/ui/stepper"
5
6export default function Pager() {
7 const [step, setStep] = React.useState(1)
8
9 return (
10 <Stepper step={step} line={false} onStepChange={setStep}>
11 <Stepper.Item />
12 <Stepper.Item />
13 <Stepper.Item />
14 <Stepper.Item />
15 </Stepper>
16 )
17}

A disabled gate that blocks forward navigation

Pass disabled on a single Stepper.Item and the component applies opacity-50 cursor-not-allowed to that indicator, sets the underlying button's disabled attribute, and silently ignores clicks — onStepChange never fires for that index. The rest of the row stays interactive. Use this to enforce "the user must finish step 2 before jumping to step 4" — track a completed boolean per stage in your wrapper state, derive disabled={!isCompleted} per item, and the indicator becomes a lock that users cannot bypass with a click.

This pairs with form validation. In a controlled wizard, set the next step's disabled flag based on whether the current step's form is valid. The user gets immediate visual feedback that "this step is gated", and the focus management stays inside the active stage until the gate opens. Combine with a Toast notification to explain why the gate is closed.

1"use client"
2
3import * as React from "react"
4import { Stepper } from "@/components/ui/stepper"
5
6export default function GatedWizard() {
7 const [step, setStep] = React.useState(1)
8 const profileComplete = false
9
10 return (
11 <Stepper step={step} onStepChange={setStep}>
12 <Stepper.Item label="Account" />
13 <Stepper.Item label="Profile" />
14 <Stepper.Item label="Review" disabled={!profileComplete} />
15 <Stepper.Item label="Done" disabled={!profileComplete} />
16 </Stepper>
17 )
18}

Next and Back controls wrapped in a card

The Stepper renders the indicator row, but moving between steps via explicit buttons is just as common as click-to-jump — a wizard inside a Dialog or a Card almost always pairs the row with a Next button on the right and a Back button on the left. Hold the step in state, pass setStep as onStepChange (so click-to-jump still works), and wire two Button elements that increment and decrement the index.

Clamp the increment with Math.min(step + 1, steps.length - 1) and the decrement with Math.max(step - 1, 0) so the buttons disable themselves at the ends — or set the disabled prop on the buttons directly. Render the active step's form between the stepper row and the controls. For an asynchronous flow where Next has to wait on a server action, swap the Next button text for a loading spinner while the action is pending.

1"use client"
2
3import * as React from "react"
4import { Stepper } from "@/components/ui/stepper"
5import { Button } from "@/components/ui/button"
6
7export default function WizardCard() {
8 const [step, setStep] = React.useState(0)
9 const last = 3
10
11 return (
12 <div className="space-y-6">
13 <Stepper step={step} onStepChange={setStep}>
14 <Stepper.Item label="Cart" />
15 <Stepper.Item label="Shipping" />
16 <Stepper.Item label="Payment" />
17 <Stepper.Item label="Review" />
18 </Stepper>
19 <div className="flex justify-between">
20 <Button
21 variant="outline"
22 disabled={step === 0}
23 onClick={() => setStep((s) => Math.max(s - 1, 0))}
24 >
25 Back
26 </Button>
27 <Button
28 disabled={step === last}
29 onClick={() => setStep((s) => Math.min(s + 1, last))}
30 >
31 Next
32 </Button>
33 </div>
34 </div>
35 )
36}
Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

Hold the step index in React.useState and pass it as the step prop on the Stepper root. Pass setStep as onStepChange and clicking any indicator below or after the current step will call your setter with that index. The component does not own internal state — the parent always drives the step number, so a wizard that needs to advance from a Next button, a form submission, or a server response just calls setStep with the new index from wherever the trigger lives.

The root walks React.Children.toArray(children) and uses React.cloneElement to inject an internal _index prop into each Stepper.Item. Each item defaults its visible content to _index + 1 when no label and no icon are provided — so dropping in four bare <Stepper.Item /> children gives you "1", "2", "3", "4" with no extra wiring. Pass a label to override the number with text, pass an icon to override with a lucide icon, and once the index falls below step the Check icon takes over.

Yes. Each Stepper.Item accepts an icon prop typed as React.ComponentType<{ className?: string }>. Pass the component reference (icon={User}) rather than a JSX element. The component renders the icon at size-4 inside the round indicator while the step is upcoming or active, and swaps to the lucide Check icon once the step is completed. Mix and match labels and icons inside the same Stepper if some stages read better as text and others as glyphs.

Pass orientation="vertical" on the Stepper root and the layout switches from flex row to flex col. The connector lines flip to w-0.5 min-h-6 self-center columns between items, the indicators stack top-to-bottom, and the rest of the API — step, onStepChange, label, icon, disabled — works exactly the same. Vertical mode is the right pick for sidebar wizards, multi-stage settings pages, and any flow tall enough that a horizontal row would crowd the available width.

Yes — pass disabled on each Stepper.Item that should be locked. The component applies opacity-50 cursor-not-allowed to that indicator, sets the underlying button element's disabled attribute, and the onStepChange callback never fires for that index. Wire disabled to a per-stage completion boolean in your wrapper state to enforce a strict order. Combine with form validation so the gate opens only when the current stage's inputs pass their checks.

Yes. The file starts with 'use client' because the root walks React.Children.toArray + React.cloneElement at render time, the items render as <button type="button"> elements with onClick handlers, and the caller owns the active step through useState. Rendering it inside a server-component page is fine — the page stays server-side and the Stepper hydrates on its own when the bundle reaches the browser. For a non-interactive progress display, drop the onStepChange prop and the indicators stay visually consistent but inert.