Stepper
Overlay guided tour - not a form wizard. Wrap real UI in StepItem to register tour targets; when open, a portal renders dim overlay, spotlight, and floating tooltip above the page.
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{" "}78 <span className="font-medium text-foreground">Start tour</span> -79 the page stays in place while a spotlight overlay walks through80 each target.81 </div>82 ) : null}8384 <StepItem85 title="Browse components"86 description="Scan the grid to find a primitive. Search narrows results without leaving the page."87 side="bottom"88 >89 <section className="space-y-3 rounded-lg border border-border p-3">90 <div className="relative">91 <Search className="absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-muted-foreground" />92 <Input93 className="h-8 pl-8 text-sm"94 placeholder="Search components…"95 defaultValue="button"96 />97 </div>98 <div className="grid grid-cols-2 gap-2 sm:grid-cols-5">99 {COMPONENTS.map((name) => (100 <div101 key={name}102 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"103 >104 <LayoutGrid className="size-3 text-muted-foreground" />105 {name}106 </div>107 ))}108 </div>109 </section>110 </StepItem>111112 <StepItem113 title="Copy the code"114 description="Install commands and snippets copy in one click - paste straight into your project."115 side="left"116 >117 <section className="flex flex-col gap-2 rounded-lg border border-border p-3 sm:flex-row sm:items-center sm:justify-between">118 <code className="rounded-md bg-muted px-2.5 py-1.5 font-mono text-xs">119 {INSTALL_SNIPPET}120 </code>121 <CopyButton value={INSTALL_SNIPPET} size="sm">122 Copy123 </CopyButton>124 </section>125 </StepItem>126127 <StepItem128 title="Customise tokens"129 description="Theme variables update the preview live. Drop this block on any settings or docs page."130 side="top"131 >132 <section className="grid gap-3 rounded-lg border border-border p-3 sm:grid-cols-2">133 <label className="flex items-center justify-between gap-3 rounded-md border border-border px-3 py-2 text-xs">134 <span className="flex items-center gap-1.5 font-medium">135 <Palette className="size-3.5 text-muted-foreground" />136 Primary hue137 </span>138 <input139 type="range"140 className="w-20 accent-primary"141 defaultValue={55}142 />143 </label>144 <label className="flex items-center justify-between gap-3 rounded-md border border-border px-3 py-2 text-xs">145 <span className="flex items-center gap-1.5 font-medium">146 <BarChart3 className="size-3.5 text-muted-foreground" />147 Radius148 </span>149 <input150 type="range"151 className="w-20 accent-primary"152 defaultValue={35}153 />154 </label>155 </section>156 </StepItem>157 </div>158 </Stepper>159160 {dismissed ? (161 <p className="border-t border-border px-4 py-3 text-center text-xs text-muted-foreground sm:px-6">162 Tour complete - the page UI above is unchanged and still usable.163 </p>164 ) : null}165166 <footer className="border-t border-border px-4 py-3 text-center text-[11px] text-muted-foreground sm:px-6">167 Guided tour overlays the page - targets stay in their natural layout.168 </footer>169 </div>170 );171}
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 |
| title | string | required | StepItem - title shown in the floating tooltip |
| description | string | undefined | StepItem - supporting copy in the floating tooltip |
| side | "top" | "bottom" | "left" | "right" | "auto" | "auto" | StepItem - preferred tooltip placement relative to the target (Floating UI flip/shift) |
| optional | boolean | false | StepItem - shows an "Optional" badge in the tooltip |
| disabled | boolean | false | StepItem - disables the step; skipped by navigation and not spotlighted |
| children | ReactNode | required | StepItem - the real UI on the page; rendered in place while the tour overlay layers above when open |