Drivn vs shadcn/ui — Stepper Compared
Drivn ships a zero-runtime Stepper with horizontal and vertical orientations and auto-numbered steps; shadcn/ui has no official Stepper component.
The first thing to know when comparing a Stepper between Drivn and shadcn/ui is that shadcn/ui does not ship one. There is no npx shadcn@latest add stepper command, no entry in the official component index, no Radix primitive named @radix-ui/react-stepper to wrap. Teams that need a multi-step wizard indicator on a shadcn-based project either copy a community implementation, paste an unofficial registry block, or compose one themselves from a Progress bar plus styled buttons. The Drivn Stepper is a single ~120 line client component with a compound Stepper.Item API, automatic step numbering, a connector line between items, and horizontal and vertical orientations baked in.
That asymmetry sets the frame. This is not a feature-for-feature head-to-head where one library does the same job slightly differently — it is a question of "we ship a Stepper, they leave you to roll one yourself". The Drivn component lives at packages/drivn/src/registry/components/stepper.ts, gets written into your project by the Drivn CLI as @/components/ui/stepper.tsx, and imports React, the local cn() utility, and a single Check icon from lucide-react. No Radix package, no peer dependency, no headless layer.
This page walks the missing-component story, the Drivn API shape, the indicator state machine, the auto-numbering behaviour, and the situations where a roll-your-own shadcn approach still makes sense.
Side-by-side comparison
| Feature | Drivn | shadcn/ui |
|---|---|---|
| Official component | Yes — ships in the registry | No — not in the shadcn/ui registry |
| Install command | npx drivn add stepper | No official command — copy a community block or build your own |
| Underlying implementation | Single client component, compound Stepper + Stepper.Item | N/A — typically composed from Progress + Button + custom CSS |
| Runtime UI dependencies | cn() utility + lucide-react Check icon — zero npm UI packages | N/A — depends on whichever community block you copy |
| Public API shape | Compound — <Stepper step={n}><Stepper.Item /></Stepper> | No standard — varies per community implementation |
| Auto step numbering | Yes — index injected via React.cloneElement when no label/icon | Not built-in |
| Orientation | 'horizontal' | 'vertical' | Depends on copy source |
| Connector line | Built-in, toggleable via line={false} | Hand-rolled per implementation |
| Indicator states | active / completed / upcoming with Check icon on completed | Not standardised |
| Click-to-jump | Built-in via onStepChange callback | Hand-rolled per implementation |
| Client component | Yes — 'use client' | Depends on implementation |
| License | MIT | MIT (when copying a community block) |
shadcn/ui has no Stepper — that is the headline
Run npx shadcn@latest add stepper and the CLI fails with "component not found". Open the shadcn/ui component index and the list jumps from Slider to Switch — no Stepper between them. The shadcn philosophy treats the Stepper as a composition problem: take the Progress bar primitive, render a row of styled Button elements above it, manage the active index in useState, and you have a stepper. That works, and many shadcn projects do exactly this, but it is a build job rather than an install job.
Drivn treats the Stepper as a primitive in its own right. The component is in the registry alongside Accordion, Dialog, and Tabs, and the Drivn CLI writes it into @/components/ui/stepper.tsx like any other piece. The file is yours after install — read it, edit it, restyle it — but the starting point is a working component, not a recipe.
1 # Drivn — one command, working component 2 npx drivn add stepper 3 4 # shadcn/ui — no official command 5 # you compose one yourself from Progress + Button + state
A compound API with auto-numbered items
The Drivn Stepper uses the dot-notation pattern that the rest of the library follows: <Stepper> is the parent that holds the step index and the optional onStepChange callback, and <Stepper.Item /> is the child that renders one indicator. Items take no props in the simplest case — the parent walks React.Children.toArray(children) and injects an internal _index prop via React.cloneElement, so the first child becomes step 0, the second step 1, and so on. The indicator content defaults to the index plus one — 1, 2, 3, 4.
Pass a label and the indicator widens with px-3 and renders the label text instead of the number. Pass an icon and the indicator renders that icon instead. Once a step is below the current step value, the indicator switches to the completed state and renders a Check icon regardless of label or icon. Read the full props table on the Stepper docs page.
1 "use client" 2 3 import * as React from "react" 4 import { Stepper } from "@/components/ui/stepper" 5 6 export default function Wizard() { 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 }
Three indicator states and a connector line
Every Stepper.Item computes its state from its _index against the parent's step: indices below step are completed, the matching index is active, every index above is upcoming. The styles.indicator object holds the visual rules for each state — completed uses bg-primary text-muted-foreground and renders the Check icon, active uses bg-accent with a border-2 border-primary ring, upcoming uses bg-accent with a faded border-border ring. The transitions are pure CSS transition-colors duration-200 so stepping forward animates without any framer-motion dependency.
Between every pair of adjacent items the Stepper renders a connector line — h-0.5 flex-1 in horizontal mode, w-0.5 min-h-6 self-center in vertical mode — coloured bg-primary when its index is below step and bg-border otherwise. Pass line={false} to drop the connectors entirely, useful for compact pagers and onboarding pills.
1 // packages/drivn/src/registry/components/stepper.ts — verbatim 2 indicator: { 3 base: cn( 4 'flex items-center justify-center shrink-0', 5 'h-8 min-w-8 rounded-full text-xs font-semibold', 6 'transition-colors duration-200 select-none', 7 '[&>svg]:size-4' 8 ), 9 text: 'px-3 shrink', 10 active: cn( 11 'bg-accent text-muted-foreground', 12 'border-2 border-primary' 13 ), 14 completed: 'bg-primary text-muted-foreground', 15 upcoming: cn( 16 'bg-accent text-muted-foreground', 17 'border-2 border-border' 18 ), 19 clickable: 'cursor-pointer', 20 disabled: 'opacity-50 cursor-not-allowed', 21 },
Where the roll-your-own shadcn path still makes sense
Two situations push you toward composing a stepper on top of shadcn primitives rather than reaching for the Drivn version. The first is a project already deep in shadcn with a strict "no other registries" rule — adding Drivn means another CLI in the build, another set of conventions, and another @/components/ui source you maintain. If your design system mandates shadcn-only, the right call is to compose a Stepper from the existing Progress primitive and a row of Button elements.
The second is a stepper with strongly custom behaviour — branching flows where step 3 conditionally jumps to step 5, animated indicator transitions with framer-motion, per-step validation that blocks forward navigation, or asynchronous step completion that swaps the indicator for a spinner. The Drivn Stepper exposes onStepChange for click-to-jump and disabled per item, but the more exotic behaviour lives in your wrapper component on top. At that point the Drivn file is still useful — copy it and extend — but a from-scratch composition can be more direct.
1 // shadcn-style composition — for reference 2 "use client" 3 import { useState } from "react" 4 import { Progress } from "@/components/ui/progress" 5 import { Button } from "@/components/ui/button" 6 7 export function ShadcnStyleStepper() { 8 const [step, setStep] = useState(0) 9 const steps = ["Cart", "Shipping", "Payment", "Review"] 10 11 return ( 12 <div className="space-y-4"> 13 <Progress value={(step / (steps.length - 1)) * 100} /> 14 <div className="flex justify-between"> 15 {steps.map((s, i) => ( 16 <Button 17 key={s} 18 variant={i === step ? "default" : "outline"} 19 onClick={() => setStep(i)} 20 > 21 {s} 22 </Button> 23 ))} 24 </div> 25 </div> 26 ) 27 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
No. The shadcn/ui registry does not include a Stepper. The official component index jumps from Slider to Switch with no Stepper between them, and there is no Radix primitive named @radix-ui/react-stepper for the shadcn wrapper to lean on. Teams that need one either copy a community implementation, paste a third-party registry block, or compose a stepper themselves from the shadcn Progress and Button primitives.
The component imports React, the local cn() utility from @/utils/cn, and one Check icon from lucide-react — nothing else. No @radix-ui packages, no class-variance-authority, no framer-motion. The whole file is around 120 lines, ships with the Drivn CLI as @/components/ui/stepper.tsx, and is yours to edit after install. The compound Stepper + Stepper.Item API is built with Object.assign — no provider package, no compound-context helper library.
The Stepper root walks React.Children.toArray(children) and uses React.cloneElement to inject an internal _index prop into each child. Each Stepper.Item then defaults its visible content to _index + 1 when no label and no icon prop is set — the first item renders "1", the second "2", and so on. Pass a label to override the number with text, pass an icon to override it with a lucide icon, and once the index drops below the current step, the Check icon takes over.
Yes. Pass orientation="vertical" and the root switches to a flex-col layout, indicators stack from top to bottom, and the connector lines become w-0.5 columns sized via min-h-6 self-center. The same indicator state machine, the same click-to-jump behaviour, the same label/icon overrides apply — only the layout axis flips. The vertical mode is the right pick for sidebar wizards, multi-step settings panels, and any flow tall enough that a horizontal row would crowd the viewport.
Yes. The file starts with 'use client' because the children injection runs at render time with React.cloneElement, the indicator buttons attach onClick handlers, and the parent owns the step index through useState in the caller. Rendering it inside a server component is fine — the parent stays server-side and the Stepper hydrates on its own when the bundle reaches the browser. For a static, non-interactive progress indicator, render a plain list with the same indicator styles and drop the onStepChange prop.
Yes, when you pass onStepChange. Each Stepper.Item is rendered as a <button type="button"> with an onClick handler that calls onStepChange with its own _index. Skip the prop and the buttons still render — they are interactive elements with focus styles — but clicking does nothing. Per-item disabled props block the callback on that specific step, useful for "the user must complete step 2 before jumping to step 4" flows. Pair the click-to-jump with form validation in your wrapper component.

