All blocks
Onboardingbeginner
Product Tour
A card-style onboarding walkthrough with dot indicators and free navigation.
Installation
Add it with the shadcn CLI, open it in v0, or read the source.
$ npx shadcn@latest add https://stepperize.com/r/product-tour.jsonDependencies
- @stepperize/react
- lucide-react
Requirements
- React 18 or later
- Tailwind CSS
Source
import { defineStepper } from "@stepperize/react";
import { Sparkles, Wand2, Zap } from "lucide-react";
import { useState } from "react";
const { Stepper } = defineStepper([
{
id: "welcome",
title: "Welcome aboard",
description: "A quick tour of what you can do here.",
},
{
id: "automate",
title: "Automate everything",
description: "Turn repetitive work into one-click flows.",
},
{
id: "ship",
title: "Ship faster",
description: "Go from idea to production in minutes.",
},
]);
const art: Record<string, { icon: typeof Zap; tint: string }> = {
welcome: { icon: Sparkles, tint: "bg-primary/10 text-primary" },
automate: { icon: Wand2, tint: "bg-chart-2/15 text-chart-2" },
ship: { icon: Zap, tint: "bg-chart-3/15 text-chart-3" },
};
export function ProductTourBlock() {
const [started, setStarted] = useState(false);
return (
<Stepper.Root className="w-full max-w-sm overflow-hidden rounded-2xl border bg-background shadow-sm">
{({ stepper }) => {
const { icon: Icon, tint } = art[stepper.current.id];
return (
<>
<div
className={`flex h-36 items-center justify-center ${tint.split(" ")[0]}`}
>
<Icon className={`size-12 ${tint.split(" ")[1]}`} />
</div>
<div className="p-6">
<Stepper.Content step={stepper.current.id}>
<h3 className="text-lg font-semibold">
{started ? "You're ready to start" : stepper.current.title}
</h3>
<p className="mt-1.5 text-sm text-muted-foreground">
{started
? "The tour is complete. Jump into the workspace."
: stepper.current.description}
</p>
</Stepper.Content>
<div className="mt-6 flex items-center justify-between">
<Stepper.List className="flex gap-1.5">
<Stepper.Items>
{(step) => (
<Stepper.Item key={step.id} step={step.id}>
<Stepper.Trigger>
<Stepper.Indicator className="block h-1.5 rounded-full transition-all data-[status=active]:w-6 data-[status=active]:bg-primary data-[status=previous]:w-1.5 data-[status=previous]:bg-primary data-[status=upcoming]:w-1.5 data-[status=upcoming]:bg-border" />
</Stepper.Trigger>
</Stepper.Item>
)}
</Stepper.Items>
</Stepper.List>
<Stepper.Actions className="flex gap-2">
{!stepper.isLast && (
<button
type="button"
onClick={() => stepper.goTo("ship")}
className="inline-flex h-9 items-center rounded-lg px-3 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
>
Skip
</button>
)}
{started ? (
<button
type="button"
onClick={() => {
setStarted(false);
stepper.reset();
}}
className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Restart tour
</button>
) : stepper.isLast ? (
<button
type="button"
onClick={() => setStarted(true)}
className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Get started
</button>
) : (
<Stepper.Next className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90">
Next
</Stepper.Next>
)}
</Stepper.Actions>
</div>
</div>
</>
);
}}
</Stepper.Root>
);
}
When to use it
A lightweight feature tour or welcome carousel where users can move freely between slides.
Accessibility
Dots are labelled with their step name; navigation is operable by keyboard and not trapped.
Customization
Swap dots for thumbnails; because navigation is non-linear, any step can be reached via `goTo`.