All components

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 button
Guided tour overlays the page - targets stay in their natural layout.
1"use client";
2
3import { 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";
10
11const COMPONENTS = [
12 "Button",
13 "Card",
14 "Input",
15 "Tabs",
16 "Dialog",
17 "Badge",
18 "Select",
19 "Sheet",
20 "Table",
21 "Toast",
22];
23
24const INSTALL_SNIPPET = "npx shadcn@latest add button";
25
26/**
27 * Overlay guided tour - StepItem marks real UI targets; the tour layers above
28 * 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);
33
34 const handleClose = () => {
35 setOpen(false);
36 setDismissed(true);
37 };
38
39 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 primitives
46 </p>
47 </div>
48 <div className="flex items-center gap-2">
49 <Badge variant="secondary" className="w-fit">
50 102 components
51 </Badge>
52 <Button
53 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 tour
61 </Button>
62 </div>
63 </header>
64
65 <Stepper
66 open={open}
67 onOpenChange={setOpen}
68 showSkip
69 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 through
80 each target.
81 </div>
82 ) : null}
83
84 <StepItem
85 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 <Input
93 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 <div
101 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>
111
112 <StepItem
113 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 Copy
123 </CopyButton>
124 </section>
125 </StepItem>
126
127 <StepItem
128 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 hue
137 </span>
138 <input
139 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 Radius
148 </span>
149 <input
150 type="range"
151 className="w-20 accent-primary"
152 defaultValue={35}
153 />
154 </label>
155 </section>
156 </StepItem>
157 </div>
158 </Stepper>
159
160 {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}
165
166 <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.

1"use client";
2
3import { 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";
8
9const 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;
14
15/**
16 * Controlled mode: parent owns step index and tour open state - useful when
17 * syncing with sidebar nav or URL hash.
18 */
19export function StepperControlledExample() {
20 const [step, setStep] = useState(0);
21 const [open, setOpen] = useState(false);
22
23 const jumpToStep = (index: number) => {
24 setStep(index);
25 setOpen(true);
26 };
27
28 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 Settings
34 </p>
35 <Button
36 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 <button
48 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 && open
54 ? "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>
64
65 <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>
75
76 <Stepper
77 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 <StepItem
87 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 TW
94 </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 Edit
101 </Button>
102 </div>
103 </StepItem>
104
105 <StepItem
106 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 <li
113 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>
122
123 <StepItem
124 title="Workspace settings"
125 description="Danger zone and API keys can be optional tour stops."
126 optional
127 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 workspace
133 </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

NameTypeDefaultDescription
open / defaultOpen / onOpenChangeboolean / boolean / (open: boolean) => voiddefaultOpen falseTour visibility - closed by default until explicitly opened (e.g. Start tour button)
defaultStepnumber0Initial step index in uncontrolled mode
currentStep / onStepChangenumber / (step: number) => voiduncontrolled if omittedControlled step index and change handler
showProgressbooleantrueShows "Step N of M" in the floating tooltip
showControlsbooleantrueToggles Previous, Next, Skip, and Finish in the tooltip
showSkipbooleanfalseShows Skip - closes the tour immediately without advancing
autoScrollbooleantrueSmoothly scrolls the active target into view on step change (respects reduced motion)
onFinish / onSkip() => voidundefinedCallbacks when Finish or Skip is clicked - tour closes after both
finishLabel / skipLabelstring"Finish" / "Skip"Custom labels for Finish and Skip buttons
ariaLabelstring"Guided tour"Accessible name for the tour dialog
classNamestringundefinedAdditional classes on the stepper children wrapper
titlestringrequiredStepItem - title shown in the floating tooltip
descriptionstringundefinedStepItem - 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)
optionalbooleanfalseStepItem - shows an "Optional" badge in the tooltip
disabledbooleanfalseStepItem - disables the step; skipped by navigation and not spotlighted
childrenReactNoderequiredStepItem - the real UI on the page; rendered in place while the tour overlay layers above when open