Search

Search the site

All components

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 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 <span className="font-medium text-foreground">Start tour</span>{" "}
78 — the page stays in place while a spotlight overlay walks through each
79 target.
80 </div>
81 ) : null}
82
83 <StepItem
84 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 <Input
92 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 <div
100 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>
110
111 <StepItem
112 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 Copy
122 </CopyButton>
123 </section>
124 </StepItem>
125
126 <StepItem
127 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 hue
136 </span>
137 <input
138 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 Radius
147 </span>
148 <input
149 type="range"
150 className="w-20 accent-primary"
151 defaultValue={35}
152 />
153 </label>
154 </section>
155 </StepItem>
156 </div>
157 </Stepper>
158
159 {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}
164
165 <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.

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
StepItem.titlestringrequiredStep title shown in the floating tooltip
StepItem.descriptionstringundefinedSupporting 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.optionalbooleanfalseShows an "Optional" badge in the tooltip
StepItem.disabledbooleanfalseDisables the step; skipped by navigation and not spotlighted
StepItem.childrenReactNoderequiredThe real UI on the page — rendered in place; tour overlay layers above when open