Coachmark Tour Flow
A product tour overlay — an SVG mask morphs the spotlight between target elements while the explanation card anchors to the best side and flies along as you step, tracking the target through scroll.
npx shadcn@latest add https://webberui.com/r/coachmark-tour-flow.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<CoachmarkTourFlow />
Installation
npx shadcn@latest add https://webberui.com/r/coachmark-tour-flow.jsonOr, once registries are configured in components.json, install it as @webberui/coachmark-tour-flow.
Usage
Describe the tour path with a steps array, where each step points at a target element (a CSS selector or an element ref). The overlay is rendered into document.body through a portal and stays pinned to the visible area of root (a scroll container); when no root is given, the whole viewport is the reference.
import * as React from "react";
import { CoachmarkTourFlow } from "@/components/ui/coachmark-tour-flow";
export function Example() {
const [open, setOpen] = React.useState(false);
return (
<>
<button data-tour="search" onClick={() => setOpen(true)}>
Start
</button>
<CoachmarkTourFlow
open={open}
onOpenChange={setOpen}
steps={[
{
target: "[data-tour='search']",
title: "Global search",
content: "Press it from any page to search.",
},
{
target: "[data-tour='settings']",
title: "Preferences",
content: "Adjust the theme and notifications here.",
placement: "top",
},
]}
/>
</>
);
}Both open and step support controlled and uncontrolled modes: passing the corresponding prop makes it controlled, otherwise the component manages it internally (use defaultOpen and defaultStep to set initial values).
Props
| Prop | Type | Default | Description |
|---|---|---|---|
steps | CoachmarkStep[] | — | The list of steps, walked in order |
open | boolean | — | Controlled: whether the tour is open |
defaultOpen | boolean | false | Default open state in uncontrolled mode |
onOpenChange | (open) => void | — | Callback for changes to the open state |
step | number | — | Controlled: current step index |
defaultStep | number | 0 | Default step index in uncontrolled mode |
onStepChange | (index) => void | — | Callback for changes to the step index |
onComplete | () => void | — | Fires when "Done" is pressed after the last step |
root | RefObject<HTMLElement> | — | Reference container for positioning and scrolling; when omitted, the viewport is used |
spotlightPadding | number | 8 | Padding of the spotlight around the target (px) |
spotlightRadius | number | 12 | Corner radius of the spotlight (px) |
overlayColor | string | rgba(10,10,10,0.6) | Dimming colour of the overlay |
scrollIntoView | boolean | true | Whether to scroll the target into view when the step changes |
showProgress | boolean | true | Show the step counter and dots |
dismissOnBackdropClick | boolean | false | Close the tour when the empty part of the overlay is clicked |
labels | { next; prev; done; skip } | — | Text for each action button |
className | string | — | Appended to the outermost overlay container |
cardClassName | string | — | Appended to the explanation card |
CoachmarkStep
| Field | Type | Description |
|---|---|---|
target | string | RefObject<HTMLElement> | Target element: a CSS selector (looked up inside root or the document) or an element ref |
title | React.ReactNode | Step title |
content | React.ReactNode | Step explanation content |
placement | "top" | "bottom" | "left" | "right" | "auto" | Side the card sticks to, overriding the default auto |
padding | number | Spotlight padding for this step, overriding the global setting |
How it works
- Mask morph: the spotlight punches a rounded rectangle out of the full-page dimming layer through an SVG
<mask>; the box'sx/y/width/height/rxare all bound to spring motion values, so it morphs smoothly to the next target when the step changes. - Flying follow: the card's position is derived live from the spotlight's spring values (
useTransform), so stepping makes the card fly along the best side to the new target and keeps following it during scroll. - Scroll tracking: while open it listens to scroll and
resizeon bothrootand the window, plus aResizeObserveron the target and container, and keeps measuring the target position withrequestAnimationFramethrottling; when the target scrolls out of view, the spotlight stays at the visible edge. - Best side: when
placementisauto, the anchoring side is chosen automatically from the space available around the target, and the card's position is clamped inside the overlay bounds to prevent overflow.
Accessibility
- The explanation card is
role="dialog", associating the title and content througharia-labelledby/aria-describedby, and focus moves into it automatically when it opens. - Keyboard:
Esccloses,→goes to the next step,←to the previous; every action also has a focusable native button. - When the user has "reduce motion" enabled, the spotlight jumps into place instantly and the fades are shortened, so scrolling and transitions no longer have spring or smoothing while the layout and functionality stay identical.
Column Transit Board
A cross-column transit board — cards fly along an arc between columns because of a programmatic state change (not a drag), while the target column opens a landing spot, neighbours in the source column close ranks, and the flying card lifts with a shadow.
Physics Tag Pile
Tags drop and pile up like physical objects, and can be picked up and thrown — a 2D physics simulation.