Welcome to Vanilla Breeze
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
Progressive-enhancement guided tour with spotlight overlay, action gating, and keyboard navigation.
A guided page tour that highlights elements sequentially with a spotlight overlay and popover card. Follows the four-layer progressive enhancement stack — content is readable without CSS or JS.
Wrap <tour-step> elements inside <page-tour>. Each step targets a page element via data-target (a CSS selector).
When JS is unavailable, the <details> wrapper provides a collapsible guide with anchor links. The "Start Tour" button is auto-wired when the component upgrades.
<page-tour id="demo-tour" data-title="Getting Started" data-trigger="manual" data-mode="passive" data-persist="none"> <details class="page-tour-guide" open> <summary class="page-tour-summary"> <span>Getting Started Tour</span> <span class="page-tour-count">3 steps</span> </summary> <ol class="page-tour-list"> <li> <tour-step data-target="#site-header"> <h3>Header</h3> <p>The main navigation lives here.</p> </tour-step> </li> <li> <tour-step data-target="#search-input" data-placement="bottom"> <h3>Search</h3> <p>Search across all content.</p> </tour-step> </li> <li> <tour-step data-target="#theme-picker" data-placement="bottom"> <h3>Theme Picker</h3> <p>Change the visual style.</p> </tour-step> </li> </ol> <button class="page-tour-start-btn" type="button">Start Tour</button> </details></page-tour>
Set data-mode="active" and data-action on steps to require user interaction before advancing. The Next button stays disabled until the action completes.
<page-tour data-title="Settings Tour" data-trigger="auto" data-mode="active" data-persist="session"> <tour-step data-target="#profile-name" data-action="input" data-action-hint="Type your name to continue" > <h3>Display Name</h3> <p>Update your name here.</p> </tour-step> <tour-step data-target="#save-btn" data-action="click" data-action-hint="Click Save to continue" > <h3>Save Settings</h3> <p>Click to apply your changes.</p> </tour-step></page-tour>
Set data-mode="forced" to prevent skipping. No Skip button, Escape is disabled, and backdrop clicks are ignored.
<page-tour data-title="Required Setup" data-trigger="auto" data-mode="forced" data-persist="local"> <tour-step data-target="#terms"> <h3>Terms of Service</h3> <p>Review the updated terms.</p> </tour-step> <tour-step data-target="#accept-checkbox" data-action="click" data-action-hint="Check the box to accept" > <h3>Accept Terms</h3> <p>You must accept to continue.</p> </tour-step></page-tour>
Use data-trigger="button" and add data-tour="tour-id" to any element to start the tour declaratively.
<!-- External trigger button --><button data-tour="editor-tour" type="button"> Take a Tour</button> <!-- The tour --><page-tour id="editor-tour" data-title="Editor Tour" data-trigger="button" data-mode="passive"> <tour-step data-target="#editor"> <h3>Editor</h3> <p>Write your content here.</p> </tour-step> <tour-step data-target="#sidebar"> <h3>Sidebar</h3> <p>Document properties and settings.</p> </tour-step></page-tour>
| Attribute | Values | Default | Description |
|---|---|---|---|
data-title | string | Tour | Tour name for aria-label and heading |
data-trigger | "auto", "manual", "button" | manual | How the tour is initiated |
data-mode | "passive", "active", "forced" | passive | Skip and action-gate behaviour |
data-persist | "none", "session", "local" | session | Where to store progress |
data-persist-key | string | — | Storage key override (defaults to page path) |
data-spotlight-padding | number | 8 | Pixel padding around the spotlight rect |
data-step | number | 0 | Current step index (0-based), reflects state |
data-active | boolean | — | Present while tour is running |
data-complete | boolean | — | Present after tour finishes or is skipped |
| Element | Required | Description |
|---|---|---|
<tour-step> | yes | Individual tour step — contains heading and description |
<details class="page-tour-guide"> | no | Layer 3 collapsible wrapper (optional) |
<button class="page-tour-start-btn"> | no | Start button inside details (optional, auto-wired) |
| Event | Detail | Description |
|---|---|---|
tour:start | { step } | Fired once when tour begins |
tour:step | { step, target, direction } | Fired on each step change |
tour:action | { step, action } | Fired when a required action completes |
tour:complete | { steps } | Fired when last step is finished |
tour:skip | { step } | Fired when user skips the tour |
const tour = document.querySelector('page-tour');const toasts = document.querySelector('toast-msg'); tour.addEventListener('tour:start', (e) => { console.log('Tour started at step', e.detail.step);}); tour.addEventListener('tour:step', (e) => { console.log('Step', e.detail.step, e.detail.direction);}); tour.addEventListener('tour:action', (e) => { console.log('Action completed:', e.detail.action);}); tour.addEventListener('tour:complete', () => { toasts.show({ message: 'Tour complete!', variant: 'success' });}); tour.addEventListener('tour:skip', (e) => { toasts.show({ message: 'Tour skipped.', variant: 'info' });});
| Method | Description |
|---|---|
tour.start(step?) | Start tour, optionally at a given step index |
tour.stop() | Stop tour silently (no event) |
tour.next() | Advance to next step |
tour.prev() | Go to previous step |
tour.goto(index) | Jump to specific step (0-based) |
tour.skip() | Skip tour (fires tour:skip) |
tour.reset() | Clear persistence and reset to step 0 |
const tour = document.querySelector('page-tour'); // Start tour at step 0tour.start(); // Navigatetour.next();tour.prev();tour.goto(2); // End tourtour.skip(); // fires tour:skiptour.stop(); // silent // Reset persistencetour.reset(); // For custom action gatingtour.dispatchEvent( new CustomEvent('tour:action', { bubbles: false }));
| Mode | Skip Allowed | Action Gate | Use Case |
|---|---|---|---|
passive | Yes | Optional | Informational tours, docs sites |
active | Yes | Required | Onboarding that validates user can perform tasks |
forced | No | Required | Compliance flows, mandatory training |
| Key | Action |
|---|---|
| Escape | Skip tour (passive/active modes only) |
| Tab | Cycle focus within the card (focus trap) |
| ArrowRight / ArrowDown | Next step |
| ArrowLeft / ArrowUp | Previous step |
| Home | First step |
| End | Last step |
The component follows a four-layer degradation strategy:
| Layer | Environment | Experience |
|---|---|---|
| 1 | No CSS, no JS | Ordered step list with headings and anchor links |
| 2 | CSS only | Styled guide card with step numbering |
| 3 | CSS + <details> | Collapsible guide with start button |
| 4 | CSS + JS | Interactive spotlight overlay with popover |
role="dialog" with aria-modal="true"aria-live="polite" region announces step transitionsprefers-reduced-motion disables all spotlight/card transitions| Property | Default | Purpose |
|---|---|---|
--tour-backdrop-color | oklch(0% 0 0 / 0.5) | Overlay background |
--tour-spotlight-ring | var(--color-primary) | Spotlight outline color |
--tour-spotlight-padding | 8px | Space around highlighted element |
--tour-spotlight-radius | var(--radius-m) | Spotlight corner radius |
--tour-card-max-width | 22rem | Maximum card width |
--tour-card-min-width | 16rem | Minimum card width |
--tour-card-offset | var(--size-s) | Gap between spotlight and card |
--tour-transition-duration | var(--duration-normal) | Spotlight/card animation |