# Spotlight Tour: What's New

> Three new features spread down a page. Each one scrolls into view while the spotlight travels to it. The Live switch stays usable while it's highlighted.

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

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

## Install

```bash
npx shadcn@latest add https://keyframeui.com/r/spotlight-tour-whats-new-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-whats-new-demo.tsx`

```tsx
"use client";

import * as React from "react";

import { SpotlightTour, type SpotlightTourStep } from "@/components/ui/spotlight-tour";

// The features are spread down the page. The tour scrolls each one into view
// while the spotlight travels to it.
const steps: SpotlightTourStep[] = [
  {
    target: "[data-tour=whats-new]",
    title: "3 new features",
    body: "Here's what shipped this week. It takes about 20 seconds.",
  },
  {
    target: "[data-tour=live]",
    title: "Live mode",
    body: "Charts now update in real time. Flip the switch to watch the numbers move.",
    interactive: true,
  },
  {
    target: "[data-tour=notes]",
    title: "Notes on charts",
    body: "Pin a note to any point on a chart, so next week's spike explains itself.",
  },
  {
    target: "[data-tour=export]",
    title: "Export to CSV",
    body: "Download any table as a CSV, with your filters applied.",
  },
];

const seed = [42, 48, 45, 53, 50, 58, 61, 57, 66, 63, 70, 74, 69, 78, 82, 79, 88, 85, 92, 96];
const weeks = [32, 38, 35, 44, 41, 47, 96, 58, 52, 55, 60, 63];
const pages = [
  { path: "/", views: "48,210", change: "+12%" },
  { path: "/pricing", views: "18,904", change: "+31%" },
  { path: "/docs/getting-started", views: "12,377", change: "+8%" },
  { path: "/blog/launch-week", views: "9,120", change: "+164%" },
  { path: "/changelog", views: "6,483", change: "-3%" },
  { path: "/careers", views: "2,941", change: "+5%" },
];

function useLive(on: boolean) {
  const [points, setPoints] = React.useState(seed);
  React.useEffect(() => {
    if (!on) return;
    const timer = window.setInterval(() => {
      setPoints((current) => {
        const last = current[current.length - 1];
        const next = Math.max(40, Math.min(120, last + Math.round(Math.random() * 14 - 6)));
        return [...current.slice(1), next];
      });
    }, 900);
    return () => window.clearInterval(timer);
  }, [on]);
  return points;
}

function line(points: number[], width: number, height: number) {
  const max = 130;
  return points
    .map((value, i) => `${i ? "L" : "M"}${((i / (points.length - 1)) * width).toFixed(1)} ${(height - (value / max) * height).toFixed(1)}`)
    .join("");
}

const card = "rounded-xl bg-white p-4 ring-1 ring-zinc-900/[0.06] dark:bg-zinc-950 dark:ring-white/[0.07]";

export function SpotlightTourWhatsNewDemo() {
  const [open, setOpen] = React.useState(false);
  const [live, setLive] = React.useState(false);
  const points = useLive(live);
  const visitors = points[points.length - 1] * 37;
  const path = line(points, 300, 96);

  return (
    <div className="w-full overflow-hidden rounded-xl border border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900">
      <div className="h-[var(--demo-height,36rem)] overflow-y-auto overscroll-contain">
        <header className="sticky top-0 z-10 flex h-14 items-center justify-between gap-3 border-b border-zinc-200/80 bg-zinc-50/85 px-4 backdrop-blur sm:px-6 dark:border-zinc-800/80 dark:bg-zinc-900/85">
          <span className="flex items-center gap-2 text-sm font-semibold text-zinc-900 dark:text-zinc-50">
            <span aria-hidden className="size-5 rounded-full bg-gradient-to-br from-emerald-400 to-teal-600" />
            Pulse
          </span>
          <button
            data-tour="whats-new"
            type="button"
            onClick={() => setOpen(true)}
            className="inline-flex h-7 items-center gap-1.5 rounded-full bg-white pr-1 pl-2.5 text-xs font-medium text-zinc-900 ring-1 ring-zinc-900/10 transition-[background-color,scale] duration-150 hover:bg-zinc-100 active:scale-[0.97] dark:bg-zinc-950 dark:text-zinc-50 dark:ring-white/10 dark:hover:bg-zinc-800"
          >
            What&apos;s new
            <span className="grid h-5 min-w-5 place-items-center rounded-full bg-emerald-500 px-1.5 text-[11px] text-white">3</span>
          </button>
        </header>

        <div className="mx-auto max-w-2xl space-y-4 px-4 py-6 sm:px-6">
          <div>
            <h2 className="text-xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">Overview</h2>
            <p className="mt-1 text-sm text-zinc-500">acme.com · Last 30 days</p>
          </div>

          <div className="grid grid-cols-3 gap-2 sm:gap-3">
            {[
              ["Visitors", "184k"],
              ["Bounce", "38%"],
              ["Avg. visit", "2m 41s"],
            ].map(([label, value]) => (
              <div key={label} className={card}>
                <p className="text-xs text-zinc-500">{label}</p>
                <p className="mt-1 text-lg font-semibold tracking-tight text-zinc-900 tabular-nums dark:text-zinc-50">{value}</p>
              </div>
            ))}
          </div>

          <section className={card}>
            <div className="flex items-center justify-between gap-3">
              <div>
                <p className="text-sm font-medium text-zinc-900 dark:text-zinc-50">Visitors right now</p>
                <p className="mt-0.5 text-2xl font-semibold tracking-tight text-zinc-900 tabular-nums dark:text-zinc-50">
                  {visitors.toLocaleString("en-US")}
                </p>
              </div>
              <label data-tour="live" className="flex cursor-pointer items-center gap-2 rounded-full py-1 pr-1 pl-2.5 text-xs font-medium text-zinc-600 dark:text-zinc-300">
                Live
                <button
                  type="button"
                  role="switch"
                  aria-checked={live}
                  onClick={() => setLive((on) => !on)}
                  className={`relative h-5 w-9 cursor-pointer rounded-full transition-colors duration-200 ${live ? "bg-emerald-500" : "bg-zinc-300 dark:bg-zinc-700"}`}
                >
                  <span
                    className={`absolute top-0.5 left-0.5 size-4 rounded-full bg-white shadow-sm transition-transform duration-200 ease-out ${live ? "translate-x-4" : ""}`}
                  />
                </button>
              </label>
            </div>
            <svg aria-hidden viewBox="0 0 300 96" preserveAspectRatio="none" className="mt-4 h-24 w-full overflow-visible">
              <path d={`${path}L300 96L0 96Z`} className="fill-emerald-500/10" />
              <path d={path} fill="none" strokeWidth={1.75} vectorEffect="non-scaling-stroke" className="stroke-emerald-500" />
            </svg>
          </section>

          <div className="pt-4">
            <h2 className="text-base font-semibold tracking-tight text-zinc-900 dark:text-zinc-50">Weekly signups</h2>
            <p className="mt-1 text-sm text-zinc-500">Week 7 was launch week.</p>
          </div>

          <section className={card}>
            <div className="relative flex h-40 items-end gap-1.5 sm:gap-2">
              {weeks.map((value, i) => (
                <div
                  key={i}
                  className={`flex-1 rounded-t-[4px] ${i === 6 ? "bg-emerald-500" : "bg-zinc-200 dark:bg-zinc-800"}`}
                  style={{ height: `${value}%` }}
                />
              ))}
              <div
                data-tour="notes"
                className="absolute top-1 left-[38%] w-44 rounded-lg bg-zinc-900 px-3 py-2 text-xs text-white shadow-lg sm:left-[46%] dark:bg-zinc-100 dark:text-zinc-900"
              >
                <p className="font-medium">Launch day</p>
                <p className="mt-0.5 text-zinc-400 dark:text-zinc-600">Front page of Hacker News for 9 hours.</p>
              </div>
            </div>
          </section>

          <div className="grid gap-3 pt-4 sm:grid-cols-2">
            {[
              ["Top referrer", "news.ycombinator.com", "31% of visits"],
              ["Top country", "United States", "42% of visits"],
              ["Top device", "Desktop", "68% of visits"],
              ["Top browser", "Chrome", "57% of visits"],
            ].map(([label, value, note]) => (
              <div key={label} className={card}>
                <p className="text-xs text-zinc-500">{label}</p>
                <p className="mt-1 truncate text-sm font-medium text-zinc-900 dark:text-zinc-50">{value}</p>
                <p className="mt-0.5 text-xs text-zinc-500">{note}</p>
              </div>
            ))}
          </div>

          <section className={`${card} p-0`}>
            <div className="flex items-center justify-between gap-3 px-4 py-3">
              <p className="text-sm font-medium text-zinc-900 dark:text-zinc-50">Top pages</p>
              <button
                data-tour="export"
                type="button"
                className="inline-flex h-7 items-center gap-1.5 rounded-md px-2.5 text-xs font-medium text-zinc-700 ring-1 ring-zinc-900/10 transition-colors hover:bg-zinc-50 dark:text-zinc-200 dark:ring-white/10 dark:hover:bg-zinc-900"
              >
                <svg aria-hidden viewBox="0 0 24 24" className="size-3.5" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
                  <path d="M12 3v12M7 10l5 5 5-5M5 21h14" />
                </svg>
                Export CSV
              </button>
            </div>
            <table className="w-full text-sm">
              <tbody>
                {pages.map((page) => (
                  <tr key={page.path} className="border-t border-zinc-100 dark:border-zinc-900">
                    <td className="max-w-0 truncate px-4 py-2.5 text-zinc-700 dark:text-zinc-300">{page.path}</td>
                    <td className="px-2 py-2.5 text-right text-zinc-900 tabular-nums dark:text-zinc-100">{page.views}</td>
                    <td
                      className={`px-4 py-2.5 text-right text-xs tabular-nums ${page.change.startsWith("-") ? "text-rose-600 dark:text-rose-400" : "text-emerald-600 dark:text-emerald-400"}`}
                    >
                      {page.change}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </section>
        </div>
      </div>

      <SpotlightTour open={open} onOpenChange={setOpen} steps={steps} />
    </div>
  );
}
```
