Onboarding coach marks: the page dims around a rounded cutout that frames each step's target, the cutout glides to the next target on a spring and the target scrolls into view, while a step card sits beside it and flips to whichever side has room. Back, next, skip and finish buttons, progress dots you can jump with, arrow keys and Escape, focus held in the card and handed back afterwards. Targets are CSS selectors or refs and are tracked through scrolling and resizing; the tour can dim the whole viewport or only its own box, let clicks through to the target, and be controlled or left to itself.
The import and the props worth knowing about, in one place.
import { TourSpotlight } from "@/components/beste/component/tour-spotlight";
const steps = [
{ target: "#search", title: "Find anything", body: "Search songs, venues and setlists.", side: "bottom" },
{ target: "#new-setlist", title: "Start a new one", body: "Copied from your last show.", side: "left" },
];
// Over the whole viewport, opened from your own state
<TourSpotlight steps={steps} open={open} onOpenChange={setOpen} onFinish={() => console.log("Tour finished")} />
// Inside a panel: selectors are searched in the children and only this box is dimmed
<TourSpotlight steps={steps} contained defaultOpen launcherLabel="Replay the tour">
<Dashboard />
</TourSpotlight>
<TourSpotlight
steps={steps}
allowTargetClick // clicks pass through the cutout
closeOnOverlayClick // clicking the dimmed area skips
dim={0.4}
labels={{ next: "Continue", finish: "Got it" }}
/>Every one of these is in the component already. They are listed because a props table cannot mention a gesture, so nothing else on this page can tell you they exist.
| Arrow right / Arrow left | Next or previous step. |
|---|---|
| Escape | Skip the tour. |
| Tab | Move between the card's buttons. Focus stays in the card until the tour closes. |
| Click a dot | Jump straight to that step. |
Read from the component's own type, so this cannot drift from what it accepts.
| Prop | Type | Default | Description |
|---|---|---|---|
steps* | TourStep[] | — | |
open | boolean | — | Whether the tour is showing. Pair with `onOpenChange` to control it. |
defaultOpen | boolean | — | |
onOpenChange | (open: boolean) => void | — | |
openDelay | number | — | Milliseconds to wait before a tour that is open on load first shows, so the page can settle. @defaultValue 3000 |
step | number | — | The current step, zero based. Pair with `onStepChange` to control it. |
defaultStep | number | — | |
onStepChange | (step: number) => void | — | |
onFinish | () => void | — | Fired when the reader presses the last step's finish button. |
onSkip | () => void | — | Fired when the reader skips, presses Escape or clicks the dimmed area (with `closeOnOverlayClick`). |
contained | boolean | — | Dim only this component's own box instead of the whole viewport, for tours inside a panel or preview. |
allowTargetClick | boolean | — | Let clicks through the cutout to the highlighted element. |
closeOnOverlayClick | boolean | — | Clicking the dimmed area skips the tour. |
ring | boolean | — | A soft ring pulses around the cutout. |
dim | number | — | How dark the dimmed area is, 0 to 1. |
launcherLabel | string | — | When set, a button with this label starts the tour again while it is closed. |
labels | TourSpotlightLabels | — | |
tone | "muted" | "outline" | "ghost" | — | |
size | "sm" | "default" | "lg" | — | |
className | string | — | |
children | React.ReactNode | — | The interface being toured. Selectors in `steps` are searched here when `contained`. |