Skip to content
WANCRAFT

B.129Cards & Containers

Product Tour

An onboarding tour that dims the page, springs a spotlight between targets and anchors a step card with collision handling.

NewReactMotionSVGTailwindadvanced

Overview

Product Tour walks people through an interface one element at a time. The page dims behind an SVG mask with a rounded cut-out around the current target, and the cut-out and the step card spring from one target to the next. Steps point at elements through refs, functions or CSS selectors scoped to a root, and each target is scrolled into view through every scrollable container it sits in (smoothly, unless reduced motion is on) without ever calling scrollIntoView. The card picks the side with room, flips when it would collide with an edge and is clamped inside the bounds, and the spotlight keeps tracking the target while anything scrolls or resizes. By default the tour is a native modal dialog labelled by its step count and title: focus moves to the primary button, Escape ends the tour and focus returns to where it was. A contained, non-modal mode scopes the overlay to a positioned container instead.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/product-tour.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/product-tour.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/product-tour.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { ProductTour, type ProductTourStep } from "@/components/ui/product-tour";
import { useRef, useState } from "react";

export function ProductTourExample() {
  const root = useRef<HTMLDivElement>(null);
  const [open, setOpen] = useState(false);
  const steps: ProductTourStep[] = [
    { target: "[data-tour=search]", title: "Search everything", body: "Jump to any project or teammate." },
    { target: "[data-tour=new]", title: "Start a project", body: "Import a repository or use a template." },
  ];

  return (
    <div ref={root}>
      <input data-tour="search" placeholder="Search" />
      <button data-tour="new">New project</button>
      <button onClick={() => setOpen(true)}>Start tour</button>
      <ProductTour
        steps={steps}
        open={open}
        onOpenChange={setOpen}
        root={root}
      />
    </div>
  );
}

03Props

PropTypeDefaultDescription
stepsProductTourStep[]—Required. { target?, title, body, placement? }; target is a selector (scoped to root), a ref or a function. No target centres the card.
openboolean—Required. Whether the tour is running.
onOpenChange(open: boolean) => void—Required. Receives false on Skip, Done and Escape.
defaultStepnumber0Step shown each time the tour opens.
onStepChange(index: number) => void—Called on every step change.
onFinish() => void—Called when Done is pressed on the last step.
rootRefObject<HTMLElement | null>—Scope for selector targets (defaults to the document).
modalbooleantrueModal <dialog> over the viewport, or a non-modal overlay inside the nearest positioned ancestor.
spotlightPaddingnumber8Space around the target, in px.
spotlightRadiusnumber12Spotlight corner radius, in px.
overlayOpacitynumber0.6Opacity of the dimmed overlay.
placement"auto" | "top" | "bottom" | "left" | "right""auto"Preferred card side; it flips when there is no room.
showProgressbooleantrueProgress dots in the card.
autoFocusbooleantrueMove focus into the card on open (and back afterwards). Turn off only for passive previews.
spring{ stiffness; damping; mass }{ 260, 30, 1 }Spring for the spotlight and card.
accentstring"#FF7448"Spotlight ring, progress and primary button colour.
labels{ next?, back?, done?, skip? }—Button labels.

04Features

  • 01SVG-mask overlay with a rounded cut-out that springs between targets
  • 02Targets by ref, function or selector scoped to a root; steps without a target are centred
  • 03Container-aware scrolling: every scrollable ancestor, then the window, smooth unless reduced motion
  • 04Card placement with flip and clamp collision handling, tracking scroll and resize every frame
  • 05Native modal <dialog> by default, or a contained non-modal overlay
  • 06Back / Next / Skip / Done, progress dots and arrow-key stepping
motion · springinput · clickinput · keyboardinput · scroll

05Accessibility

  • Modal mode uses <dialog> with showModal(): the rest of the page is inert, and the dialog is labelled by the step count and title and described by the body.
  • Focus moves to the primary button when the tour opens and returns to the previously focused element when it closes; Escape ends the tour.
  • Step changes are announced in a polite live region ("Step 2 of 5: Start a project"); ArrowLeft / ArrowRight step through while focus is in the card.
  • The overlay, spotlight and progress dots are aria-hidden; the non-modal mode renders the card as role="dialog" with aria-modal="false".
  • prefers-reduced-motion jumps the spotlight and card, scrolls instantly and removes the fades.

06Limitations

  • Highlighted targets are not interactive while a modal tour is open (the page is inert).
  • The contained mode needs a positioned ancestor (e.g. relative) to size its overlay.