A first tour of a dashboard: the avatar, the toolbar, the New button and the sidebar. The hole goes from a circle to a long pill to a button to a tall panel. Clicking New moves the tour on.
@keyframeui
registry/demos/spotlight-tour-demo.tsx
"use client";
import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import { SpotlightTour, type SpotlightTourStep } from "@/components/ui/spotlight-tour";
const steps: SpotlightTourStep[] = [
{
target: "[data-tour=avatar]",
title: "Your profile",
body: "Switch accounts, change your theme and manage billing from here.",
},
{
target: "[data-tour=toolbar]",
title: "Quick actions",
body: "Search, your inbox and help. Everything you reach for daily, one click away.",
},
{
target: "[data-tour=new]",
title: "Create a project",
body: "Start from scratch or from a template. Click New to try it.",
advanceOnClick: true,
},
{
target: "[data-tour=sidebar]",
title: "Your workspace",
body: "Projects, deployments and settings all live here. That's the whole tour.",
},
];
const glyph = (d: string) => (
<svg aria-hidden viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth={1.75} strokeLinecap="round" strokeLinejoin="round">
<path d={d} />
</svg>
);
const nav = [
{ label: "Projects", icon: glyph("M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z") },
{ label: "Deployments", icon: glyph("M13 2 3 14h9l-1 8 10-12h-9l1-8z") },
{ label: "Analytics", icon: glyph("M4 20V10M10 20V4M16 20v-7M22 20H2") },
{ label: "Domains", icon: glyph("M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18") },
{ label: "Settings", icon: glyph("M4 6h9M17 6h3M4 12h3M11 12h9M4 18h11M19 18h1M15 4v4M9 10v4M17 16v4") },
];
const tools = [
{ label: "Search", icon: glyph("M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14ZM20 20l-3.5-3.5") },
{ label: "Inbox", icon: glyph("M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10.3 21a1.94 1.94 0 0 0 3.4 0") },
{ label: "Help", icon: glyph("M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01") },
];
type Project = { id: string; name: string; meta: string; color: string; status: "Live" | "Building" | "Draft" };
const projects: Project[] = [
{ id: "storefront", name: "Storefront", meta: "Updated 2h ago", color: "bg-orange-500", status: "Live" },
{ id: "docs", name: "Docs", meta: "Updated yesterday", color: "bg-sky-500", status: "Live" },
{ id: "dashboard", name: "Admin dashboard", meta: "Updated 3 days ago", color: "bg-violet-500", status: "Building" },
{ id: "blog", name: "Blog", meta: "Updated last week", color: "bg-emerald-500", status: "Live" },
];
const status = {
Live: "bg-emerald-500/10 text-emerald-700 dark:text-emerald-400",
Building: "bg-amber-500/10 text-amber-700 dark:text-amber-400",
Draft: "bg-zinc-500/10 text-zinc-600 dark:text-zinc-400",
};
const panel = "rounded-lg bg-white ring-1 ring-zinc-900/[0.06] dark:bg-zinc-950 dark:ring-white/[0.06]";
export function SpotlightTourDemo() {
const [open, setOpen] = React.useState(false);
const [list, setList] = React.useState(projects);
const addProject = () =>
setList((current) =>
current.length >= 6
? current
: [{ id: `draft-${current.length}`, name: "Untitled project", meta: "Just now", color: "bg-zinc-400", status: "Draft" }, ...current],
);
return (
<div className="flex h-[28rem] w-full max-w-3xl gap-2 overflow-hidden rounded-xl border border-zinc-200 bg-zinc-100 p-2 dark:border-zinc-800 dark:bg-zinc-900">
<aside data-tour="sidebar" className={`${panel} flex w-12 shrink-0 flex-col gap-0.5 p-1.5 sm:w-44 sm:p-2`}>
<span className="mb-3 flex h-8 items-center gap-2 px-1.5 text-sm font-semibold text-zinc-900 dark:text-zinc-50">
<span aria-hidden className="size-5 shrink-0 rounded-md bg-gradient-to-br from-orange-400 to-rose-500" />
<span className="hidden sm:inline">Acme</span>
</span>
{nav.map((item, i) => (
<span
key={item.label}
className={
i === 0
? "flex h-8 items-center gap-2.5 rounded-md bg-zinc-100 px-2 text-sm text-zinc-900 dark:bg-zinc-900 dark:text-zinc-50"
: "flex h-8 items-center gap-2.5 rounded-md px-2 text-sm text-zinc-500"
}
>
{item.icon}
<span className="hidden sm:inline">{item.label}</span>
</span>
))}
<div className="mt-auto hidden rounded-md bg-zinc-50 p-2.5 sm:block dark:bg-zinc-900">
<p className="text-xs font-medium text-zinc-900 dark:text-zinc-100">Hobby plan</p>
<div className="mt-2 h-1 rounded-full bg-zinc-200 dark:bg-zinc-800">
<div className="h-full w-2/3 rounded-full bg-zinc-900 dark:bg-zinc-100" />
</div>
<p className="mt-1.5 text-[11px] text-zinc-500">4 of 6 projects</p>
</div>
</aside>
<div className="flex min-w-0 flex-1 flex-col gap-2">
<header className="flex h-10 shrink-0 items-center justify-between gap-3 pl-2">
<p className="truncate text-sm text-zinc-500">
Acme <span className="px-1 text-zinc-300 dark:text-zinc-700">/</span>
<span className="text-zinc-900 dark:text-zinc-50">Projects</span>
</p>
<div className="flex items-center gap-2">
<div data-tour="toolbar" className="flex items-center rounded-full bg-white p-1 ring-1 ring-zinc-900/[0.06] dark:bg-zinc-950 dark:ring-white/[0.06]">
{tools.map((tool) => (
<button
key={tool.label}
type="button"
aria-label={tool.label}
className="grid size-7 place-items-center rounded-full text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-900 dark:hover:text-zinc-50"
>
{tool.icon}
</button>
))}
</div>
<button
data-tour="avatar"
type="button"
aria-label="Account"
className="grid size-9 place-items-center rounded-full bg-gradient-to-br from-sky-400 to-indigo-500 text-xs font-semibold text-white ring-2 ring-white dark:ring-zinc-950"
>
JL
</button>
</div>
</header>
<main className={`${panel} flex min-h-0 flex-1 flex-col overflow-hidden`}>
<div className="flex items-center justify-between gap-3 border-b border-zinc-100 px-4 py-3 dark:border-zinc-900">
<h3 className="text-sm font-medium text-zinc-900 dark:text-zinc-50">Projects</h3>
<button
data-tour="new"
type="button"
onClick={addProject}
className="inline-flex h-8 items-center gap-1.5 rounded-lg bg-zinc-900 px-3 text-sm font-medium text-white transition-[background-color,scale] duration-150 hover:bg-zinc-800 active:scale-[0.97] dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-200"
>
<svg aria-hidden viewBox="0 0 24 24" className="size-3.5" fill="none" stroke="currentColor" strokeWidth={2.25} strokeLinecap="round">
<path d="M12 5v14M5 12h14" />
</svg>
New
</button>
</div>
<ul className="min-h-0 flex-1 overflow-hidden px-2 py-1.5">
<AnimatePresence initial={false}>
{list.map((project) => (
<motion.li
key={project.id}
layout
initial={{ opacity: 0, y: -6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", visualDuration: 0.35, bounce: 0.15 }}
className="flex h-12 items-center gap-3 rounded-md px-2"
>
<span aria-hidden className={`size-2 shrink-0 rounded-full ${project.color}`} />
<span className="min-w-0 flex-1">
<span className="block truncate text-sm text-zinc-900 dark:text-zinc-100">{project.name}</span>
<span className="block truncate text-xs text-zinc-500">{project.meta}</span>
</span>
<span className={`rounded-full px-2 py-0.5 text-[11px] font-medium ${status[project.status]}`}>
{project.status}
</span>
</motion.li>
))}
</AnimatePresence>
</ul>
<div className="flex items-center justify-between gap-3 border-t border-zinc-100 bg-zinc-50/60 px-4 py-3 dark:border-zinc-900 dark:bg-zinc-900/40">
<p className="text-sm text-pretty text-zinc-600 dark:text-zinc-400">New here? Take a 20-second tour.</p>
<button
type="button"
onClick={() => setOpen(true)}
className="h-8 shrink-0 rounded-lg px-3 text-sm font-medium text-zinc-900 ring-1 ring-zinc-900/10 transition-[background-color,scale] duration-150 hover:bg-white active:scale-[0.97] dark:text-zinc-50 dark:ring-white/10 dark:hover:bg-zinc-900"
>
Start tour
</button>
</div>
</main>
</div>
<SpotlightTour open={open} onOpenChange={setOpen} steps={steps} />
</div>
);
}

























