# Spotlight Tour: Product Onboarding

> 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.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/spotlight-tour-demo).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/spotlight-tour-demo
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/keyframeui/spotlight-tour-demo-light-f5ac655ae02f55d6.png
- Registry item: https://keyframeui.com/r/spotlight-tour-demo.json
- Listed: 2026-10-14, source last changed 2026-10-14

## Install

```bash
npx shadcn@latest add https://keyframeui.com/r/spotlight-tour-demo.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://keyframeui.com/r/spotlight-tour.json`

## Files

### `registry/demos/spotlight-tour-demo.tsx`

```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>
  );
}
```
