Detent web components
By Shane Logsdon

Overlays & menus

Product Tour

Implemented

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
Start tour New project

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

  • open recorded state; no setter
  • closed recorded state; no setter
  • light theme recorded state; no setter
  • reduced motion recorded state; no setter
  • completed recorded 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-click
  • open
  • start-at

Properties

  • onClose
  • onStepChange
  • steps

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.tsx sha256 0c0c15437b71…

Native files

The native module and its artifacts:

  • src/components/product-tour/product-tour.ts
  • src/components/product-tour/styles.css
  • src/components/product-tour/define.ts
  • docs/components/product-tour.md
  • site/examples/product-tour.ts
  • tests/browser/product-tour.spec.ts
  • evidence/components/product-tour.md

API

Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.

Attributes

Attributes
AttributePropertyTypeDefaultNotes
close-on-overlay-clickcloseOnOverlayClickbooleanfalsePresence semantics: present means true, absent means false.
openopenboolean—Presence semantics: present means true, absent means false.
start-atstartAtnumber0Kebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
onClose(reason: TourCloseReason) => void—Property-only: assign from script; not accepted as an attribute.
onStepChange(index: number) => voidundefinedProperty-only: assign from script; not accepted as an attribute.
stepsTourStep[]—Property-only: assign from script; not accepted as an attribute.

Events

Events
EventUpstream propPayload typeNotes
detent-closeonClose(reason: TourCloseReason) => voidComposed, bubbles; fires only for a user action.
detent-step-changeonStepChange(index: number) => voidComposed, 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.
Previous: Pricing Next: Profile Card

Docs

Components

Project