Stepper
Overlay guided tour — mark existing UI with StepItem, start a portal spotlight tour with dim overlay, floating tooltip, and keyboard navigation.
Default
Registry page with Start tour — spotlight overlay walks browse, copy, and theme targets
Component Registry
Browse, copy, and theme primitives
102 components
Click Start tour — the page stays in place while a spotlight overlay walks through each target.
Button
Card
Input
Tabs
Dialog
Badge
Select
Sheet
Table
Toast
npx shadcn@latest add buttondefault-example.tsx
1"use client";23import { useState } from "react";4import { BarChart3, LayoutGrid, Palette, Search, Sparkles } from "lucide-react";5import { Badge } from "@/components/ui/badge";6import { Button } from "@/components/ui/button";7import { Input } from "@/components/ui/input";8import { CopyButton } from "@/registry/ui/copy-button";9import { StepItem, Stepper } from "@/registry/ui/stepper";1011const COMPONENTS = [12 "Button",13 "Card",14 "Input",15 "Tabs",16 "Dialog",17 "Badge",18 "Select",19 "Sheet",20 "Table",21 "Toast",22];2324const INSTALL_SNIPPET = "npx shadcn@latest add button";2526/**27 * Overlay guided tour — StepItem marks real UI targets; the tour layers above28 * the page via portal when the user clicks Start Tour.29 */30export function StepperDefaultExample() {31 const [open, setOpen] = useState(false);32 const [dismissed, setDismissed] = useState(false);3334 const handleClose = () => {35 setOpen(false);36 setDismissed(true);37 };3839 return (40 <div className="mx-auto w-full max-w-3xl rounded-xl border border-border bg-background shadow-sm">41 <header className="flex flex-col gap-3 border-b border-border px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-6">42 <div>43 <p className="text-sm font-semibold">Component Registry</p>44 <p className="text-xs text-muted-foreground">45 Browse, copy, and theme primitives46 </p>47 </div>48 <div className="flex items-center gap-2">49 <Badge variant="secondary" className="w-fit">50 102 components51 </Badge>52 <Button53 type="button"54 size="sm"55 variant={open ? "secondary" : "default"}56 onClick={() => setOpen(true)}57 disabled={open}58 >59 <Sparkles className="size-3.5" data-icon="inline-start" />60 Start tour61 </Button>62 </div>63 </header>6465 <Stepper66 open={open}67 onOpenChange={setOpen}68 showSkip69 skipLabel="Skip tour"70 finishLabel="Done"71 onFinish={handleClose}72 onSkip={handleClose}73 >74 <div className="space-y-4 px-4 py-5 sm:px-6">75 {!dismissed ? (76 <div className="rounded-lg border border-dashed border-border/80 bg-muted/20 px-3 py-2 text-xs text-muted-foreground">77 Click <span className="font-medium text-foreground">Start tour</span>{" "}78 — the page stays in place while a spotlight overlay walks through each79 target.80 </div>81 ) : null}8283 <StepItem84 title="Browse components"85 description="Scan the grid to find a primitive. Search narrows results without leaving the page."86 side="bottom"87 >88 <section className="space-y-3 rounded-lg border border-border p-3">89 <div className="relative">90 <Search className="absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-muted-foreground" />91 <Input92 className="h-8 pl-8 text-sm"93 placeholder="Search components…"94 defaultValue="button"95 />96 </div>97 <div className="grid grid-cols-2 gap-2 sm:grid-cols-5">98 {COMPONENTS.map((name) => (99 <div100 key={name}101 className="flex items-center justify-center gap-1.5 rounded-md border border-border bg-muted/30 px-2 py-3 text-xs font-medium"102 >103 <LayoutGrid className="size-3 text-muted-foreground" />104 {name}105 </div>106 ))}107 </div>108 </section>109 </StepItem>110111 <StepItem112 title="Copy the code"113 description="Install commands and snippets copy in one click — paste straight into your project."114 side="left"115 >116 <section className="flex flex-col gap-2 rounded-lg border border-border p-3 sm:flex-row sm:items-center sm:justify-between">117 <code className="rounded-md bg-muted px-2.5 py-1.5 font-mono text-xs">118 {INSTALL_SNIPPET}119 </code>120 <CopyButton value={INSTALL_SNIPPET} size="sm">121 Copy122 </CopyButton>123 </section>124 </StepItem>125126 <StepItem127 title="Customise tokens"128 description="Theme variables update the preview live. Drop this block on any settings or docs page."129 side="top"130 >131 <section className="grid gap-3 rounded-lg border border-border p-3 sm:grid-cols-2">132 <label className="flex items-center justify-between gap-3 rounded-md border border-border px-3 py-2 text-xs">133 <span className="flex items-center gap-1.5 font-medium">134 <Palette className="size-3.5 text-muted-foreground" />135 Primary hue136 </span>137 <input138 type="range"139 className="w-20 accent-primary"140 defaultValue={55}141 />142 </label>143 <label className="flex items-center justify-between gap-3 rounded-md border border-border px-3 py-2 text-xs">144 <span className="flex items-center gap-1.5 font-medium">145 <BarChart3 className="size-3.5 text-muted-foreground" />146 Radius147 </span>148 <input149 type="range"150 className="w-20 accent-primary"151 defaultValue={35}152 />153 </label>154 </section>155 </StepItem>156 </div>157 </Stepper>158159 {dismissed ? (160 <p className="border-t border-border px-4 py-3 text-center text-xs text-muted-foreground sm:px-6">161 Tour complete — the page UI above is unchanged and still usable.162 </p>163 ) : null}164165 <footer className="border-t border-border px-4 py-3 text-center text-[11px] text-muted-foreground sm:px-6">166 Guided tour overlays the page — targets stay in their natural layout.167 </footer>168 </div>169 );170}
Controlled
Settings layout with controlled open + step synced to sidebar navigation
Controlled step: 0 · Tour: closed
TW
Taylor Ward
@taylor
- Deploy alertsOn
- PR reviewsOn
- Weekly digestOn
API keys, billing, and team roles.
controlled-example.tsx
1"use client";23import { useState } from "react";4import { Bell, Settings, Sparkles, User } from "lucide-react";5import { Button } from "@/components/ui/button";6import { StepItem, Stepper } from "@/registry/ui/stepper";7import { cn } from "@/lib/utils";89const NAV_ITEMS = [10 { id: "profile", label: "Profile", icon: User },11 { id: "notifications", label: "Notifications", icon: Bell },12 { id: "settings", label: "Settings", icon: Settings },13] as const;1415/**16 * Controlled mode: parent owns step index and tour open state — useful when17 * syncing with sidebar nav or URL hash.18 */19export function StepperControlledExample() {20 const [step, setStep] = useState(0);21 const [open, setOpen] = useState(false);2223 const jumpToStep = (index: number) => {24 setStep(index);25 setOpen(true);26 };2728 return (29 <div className="mx-auto flex w-full max-w-2xl flex-col gap-4 sm:flex-row">30 <aside className="shrink-0 rounded-lg border border-border bg-muted/20 p-2 sm:w-44">31 <div className="flex items-center justify-between gap-2 px-2 py-1">32 <p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">33 Settings34 </p>35 <Button36 type="button"37 variant="ghost"38 size="icon-sm"39 aria-label="Start guided tour"40 onClick={() => setOpen(true)}41 >42 <Sparkles className="size-3.5" />43 </Button>44 </div>45 <nav className="space-y-0.5">46 {NAV_ITEMS.map((item, index) => (47 <button48 key={item.id}49 type="button"50 onClick={() => jumpToStep(index)}51 className={cn(52 "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs transition-colors",53 step === index && open54 ? "bg-background font-medium shadow-sm"55 : "text-muted-foreground hover:bg-background/60",56 )}57 >58 <item.icon className="size-3.5" />59 {item.label}60 </button>61 ))}62 </nav>63 </aside>6465 <div className="min-w-0 flex-1 space-y-3">66 <p className="text-xs text-muted-foreground">67 Controlled step:{" "}68 <span className="font-medium text-foreground">{step}</span>69 {" · "}70 Tour:{" "}71 <span className="font-medium text-foreground">72 {open ? "open" : "closed"}73 </span>74 </p>7576 <Stepper77 open={open}78 onOpenChange={setOpen}79 currentStep={step}80 onStepChange={setStep}81 showProgress={false}82 finishLabel="Close tour"83 onFinish={() => setOpen(false)}84 ariaLabel="Settings page guided tour"85 >86 <StepItem87 title="Profile panel"88 description="Avatar, display name, and public handle live here — the tour spotlights this block without moving it."89 side="right"90 >91 <div className="flex items-center gap-3 rounded-lg border border-border p-4">92 <div className="flex size-10 items-center justify-center rounded-full bg-muted text-sm font-medium">93 TW94 </div>95 <div>96 <p className="text-sm font-medium">Taylor Ward</p>97 <p className="text-xs text-muted-foreground">@taylor</p>98 </div>99 <Button size="sm" variant="outline" className="ml-auto">100 Edit101 </Button>102 </div>103 </StepItem>104105 <StepItem106 title="Notification rules"107 description="Toggle channels per event type. Wrap any list or table — no target IDs required."108 side="bottom"109 >110 <ul className="divide-y divide-border rounded-lg border border-border p-2 text-xs">111 {["Deploy alerts", "PR reviews", "Weekly digest"].map((rule) => (112 <li113 key={rule}114 className="flex items-center justify-between px-2 py-2.5"115 >116 <span>{rule}</span>117 <span className="text-muted-foreground">On</span>118 </li>119 ))}120 </ul>121 </StepItem>122123 <StepItem124 title="Workspace settings"125 description="Danger zone and API keys can be optional tour stops."126 optional127 side="top"128 >129 <div className="space-y-2 rounded-lg border border-border p-4 text-xs text-muted-foreground">130 <p>API keys, billing, and team roles.</p>131 <Button size="sm" variant="secondary">132 Manage workspace133 </Button>134 </div>135 </StepItem>136 </Stepper>137 </div>138 </div>139 );140}
Installation & source
Install via the shadcn CLI or copy the registry files manually.
bash
npx shadcn@latest add @tt-ui/stepper
Props
| Name | Type | Default | Description |
|---|---|---|---|
| open / defaultOpen / onOpenChange | boolean / boolean / (open: boolean) => void | defaultOpen false | Tour visibility — closed by default until explicitly opened (e.g. Start tour button) |
| defaultStep | number | 0 | Initial step index in uncontrolled mode |
| currentStep / onStepChange | number / (step: number) => void | uncontrolled if omitted | Controlled step index and change handler |
| showProgress | boolean | true | Shows "Step N of M" in the floating tooltip |
| showControls | boolean | true | Toggles Previous, Next, Skip, and Finish in the tooltip |
| showSkip | boolean | false | Shows Skip — closes the tour immediately without advancing |
| autoScroll | boolean | true | Smoothly scrolls the active target into view on step change (respects reduced motion) |
| onFinish / onSkip | () => void | undefined | Callbacks when Finish or Skip is clicked — tour closes after both |
| finishLabel / skipLabel | string | "Finish" / "Skip" | Custom labels for Finish and Skip buttons |
| ariaLabel | string | "Guided tour" | Accessible name for the tour dialog |
| className | string | undefined | Additional classes on the stepper children wrapper |
| StepItem.title | string | required | Step title shown in the floating tooltip |
| StepItem.description | string | undefined | Supporting copy in the floating tooltip |
| StepItem.side | "top" | "bottom" | "left" | "right" | "auto" | "auto" | Preferred tooltip placement relative to the target (Floating UI flip/shift) |
| StepItem.optional | boolean | false | Shows an "Optional" badge in the tooltip |
| StepItem.disabled | boolean | false | Disables the step; skipped by navigation and not spotlighted |
| StepItem.children | ReactNode | required | The real UI on the page — rendered in place; tour overlay layers above when open |