Overlays & menus
Product Tour
Interactive walkthrough with spring spotlight cutout and responsive docking
detent-product-tour
Port status
- Catalog status
- implemented
- Origin
- upstream
- Phase
- 3
- Wave
- phase-3-overlays
- Group
- Overlays & menus
9.7 KB gzip for the class module · inlines the shared spring
Variants
Arrow keys move between steps, Escape dismisses and returns focus to the trigger.
Recorded states
openrecorded state; no setterclosedrecorded state; no setterlight themerecorded state; no setterreduced motionrecorded state; no settercompletedrecorded state; no setter
Usage
Import
import '/components/product-tour/define.js';
// or register it yourself
import { DetentProductTour } from '/components/product-tour/product-tour.js';
Attributes
close-on-overlay-clickopenstart-at
Properties
onCloseonStepChangesteps
Slots
No slots are recorded for this component.
Events
detent-close— {}detent-step-change— {}
Example
Runnable example: site/examples/product-tour.ts. The snippet below is generated from the catalogued attributes.
<detent-product-tour
close-on-overlay-click
open
start-at="0"
></detent-product-tour>
Property-only values
const element = document.querySelector('detent-product-tour');
if (element !== null) {
// steps: TourStep[]
element.steps = [];
// onClose: (reason: TourCloseReason) => void
element.onClose = () => {};
// onStepChange: (index: number) => void
element.onStepChange = () => {};
}
Source
The pinned upstream source for this row:
src/components/ui/ProductTour.tsxsha256 0c0c15437b71…
Native files
The native module and its artifacts:
src/components/product-tour/product-tour.tssrc/components/product-tour/styles.csssrc/components/product-tour/define.tsdocs/components/product-tour.mdsite/examples/product-tour.tstests/browser/product-tour.spec.tsevidence/components/product-tour.md
API
Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.
Attributes
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
close-on-overlay-click | closeOnOverlayClick | boolean | false | Presence semantics: present means true, absent means false. |
open | open | boolean | — | Presence semantics: present means true, absent means false. |
start-at | startAt | number | 0 | Kebab-case attribute; camelCase property. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
onClose | (reason: TourCloseReason) => void | — | Property-only: assign from script; not accepted as an attribute. |
onStepChange | (index: number) => void | undefined | Property-only: assign from script; not accepted as an attribute. |
steps | TourStep[] | — | Property-only: assign from script; not accepted as an attribute. |
Events
| Event | Upstream prop | Payload type | Notes |
|---|---|---|---|
detent-close | onClose | (reason: TourCloseReason) => void | Composed, bubbles; fires only for a user action. |
detent-step-change | onStepChange | (index: number) => void | Composed, bubbles; fires only for a user action. |
Slots
No slots are recorded for this component.
CSS customisation
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React portals replaced with native dialog/top-layer and DOM insertion.
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- No portal. Upstream createPortal-ed into document.body; the native port is a modal <dialog> in the top layer, which is strictly stronger: no ancestor can clip the overlay and the page behind is inert (PC-9.8).
- ProductTourDemo is not shipped. The row lists it as an extra export; it is upstream's Tailwind showcase page, not a component. The same behaviour lives in site/examples/product-tour.ts so the tag/export mapping stays one-to-one (PC-2.1).
- body is a string, not a ReactNode. Rendered with textContent (R-1.1, PC-11.1, PC-11.4: there is no HTML escape hatch).
- target is a selector string only. The upstream () => HTMLElement form is a function and cannot cross the plain-data boundary (PC-3.1, PC-6.3).
- The card is a native dialog. Upstream put role="dialog"/aria-modal on a div and trapped Tab by hand over button elements; the native modal provides the semantics, the inert background and Escape via cancel (PC-12.1, PC-12.4).
- The step announcement is a polite status node, updated once per step (Step N of M: title); upstream relied on aria-live around animated content, which can announce partial content twice (PC-12.5).
- Springs come from the shared helper. SPOT (380/38/0.9) drives the cutout, CARD (320/34/0.9) the card position/height and the overlay fade; the step content slides as upstream's AnimatePresence mode="popLayout" (enter from x d*20 and exit to x -d*20, each with opacity 0 and blur(3px), on the 0.2 s EASE_OUT cubic-bezier(0.23, 1, 0.32, 1) tween through WAAPI; the leaving copy is aria-hidden and inert, and announcements stay on the single status node); the card height is spring-driven from a ResizeObserver measurement (PC-10.2). Reduced motion swaps the content at once.
- The step dots keep their accessible names and use aria-current instead of a className; the idle dot bar uses the muted text token so it clears the non-text contrast threshold (PC-9.5, WCAG 1.4.11).
- className is replaced by documented parts and custom properties.
What it does
An interactive walkthrough: a spring-animated SVG-mask spotlight around the current step target plus one card that floats next to the target on desktop and docks to the bottom of the viewport below 640 px. There is no trigger in the component itself — the page opens the tour by setting open.
Upstream behaviour
- Spring-animated SVG mask spotlight that smoothly transitions between target elements
- Automatic scroll-into-view with scroll settlement detection before positioning
- Responsive viewport placement: floating tooltip on desktop, docked bottom sheet on mobile
- Complete keyboard controls (Left/Right arrows to navigate, Escape to dismiss, Tab trapping)
- Dynamic height animation with ResizeObserver as step copy changes
- Reduced motion support and full dark and light mode styling
Differences from upstream
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React portals replaced with native dialog/top-layer and DOM insertion.
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- No portal. Upstream createPortal-ed into document.body; the native port is a modal <dialog> in the top layer, which is strictly stronger: no ancestor can clip the overlay and the page behind is inert (PC-9.8).
- ProductTourDemo is not shipped. The row lists it as an extra export; it is upstream's Tailwind showcase page, not a component. The same behaviour lives in site/examples/product-tour.ts so the tag/export mapping stays one-to-one (PC-2.1).
- body is a string, not a ReactNode. Rendered with textContent (R-1.1, PC-11.1, PC-11.4: there is no HTML escape hatch).
- target is a selector string only. The upstream () => HTMLElement form is a function and cannot cross the plain-data boundary (PC-3.1, PC-6.3).
- The card is a native dialog. Upstream put role="dialog"/aria-modal on a div and trapped Tab by hand over button elements; the native modal provides the semantics, the inert background and Escape via cancel (PC-12.1, PC-12.4).
- The step announcement is a polite status node, updated once per step (Step N of M: title); upstream relied on aria-live around animated content, which can announce partial content twice (PC-12.5).
- Springs come from the shared helper. SPOT (380/38/0.9) drives the cutout, CARD (320/34/0.9) the card position/height and the overlay fade; the step content slides as upstream's AnimatePresence mode="popLayout" (enter from x d*20 and exit to x -d*20, each with opacity 0 and blur(3px), on the 0.2 s EASE_OUT cubic-bezier(0.23, 1, 0.32, 1) tween through WAAPI; the leaving copy is aria-hidden and inert, and announcements stay on the single status node); the card height is spring-driven from a ResizeObserver measurement (PC-10.2). Reduced motion swaps the content at once.
- The step dots keep their accessible names and use aria-current instead of a className; the idle dot bar uses the muted text token so it clears the non-text contrast threshold (PC-9.5, WCAG 1.4.11).
- className is replaced by documented parts and custom properties.