WebberUI

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.

Loading preview…
npx shadcn@latest add https://webberui.com/r/coachmark-tour-flow.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

8
12
<CoachmarkTourFlow />

Installation

npx shadcn@latest add https://webberui.com/r/coachmark-tour-flow.json

Or, 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

PropTypeDefaultDescription
stepsCoachmarkStep[]The list of steps, walked in order
openbooleanControlled: whether the tour is open
defaultOpenbooleanfalseDefault open state in uncontrolled mode
onOpenChange(open) => voidCallback for changes to the open state
stepnumberControlled: current step index
defaultStepnumber0Default step index in uncontrolled mode
onStepChange(index) => voidCallback for changes to the step index
onComplete() => voidFires when "Done" is pressed after the last step
rootRefObject<HTMLElement>Reference container for positioning and scrolling; when omitted, the viewport is used
spotlightPaddingnumber8Padding of the spotlight around the target (px)
spotlightRadiusnumber12Corner radius of the spotlight (px)
overlayColorstringrgba(10,10,10,0.6)Dimming colour of the overlay
scrollIntoViewbooleantrueWhether to scroll the target into view when the step changes
showProgressbooleantrueShow the step counter and dots
dismissOnBackdropClickbooleanfalseClose the tour when the empty part of the overlay is clicked
labels{ next; prev; done; skip }Text for each action button
classNamestringAppended to the outermost overlay container
cardClassNamestringAppended to the explanation card

CoachmarkStep

FieldTypeDescription
targetstring | RefObject<HTMLElement>Target element: a CSS selector (looked up inside root or the document) or an element ref
titleReact.ReactNodeStep title
contentReact.ReactNodeStep explanation content
placement"top" | "bottom" | "left" | "right" | "auto"Side the card sticks to, overriding the default auto
paddingnumberSpotlight 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's x/y/width/height/rx are 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 resize on both root and the window, plus a ResizeObserver on the target and container, and keeps measuring the target position with requestAnimationFrame throttling; when the target scrolls out of view, the spotlight stays at the visible edge.
  • Best side: when placement is auto, 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 through aria-labelledby / aria-describedby, and focus moves into it automatically when it opens.
  • Keyboard: Esc closes, 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.

On this page