@tapestry-ui/tour
v0.3.0
Published
A WCAG 2.1 AA product tour for Preact — spotlight, coach marks, keyboard, headless behavior, skin via CSS. The sequence is the component's; the page is yours.
Maintainers
Readme
@tapestry-ui/tour
A product tour for Preact: spotlight the thing, explain it, move on. WCAG 2.1 AA, headless behavior, skinned entirely through CSS.
Part of Tapestry UI.
npm i @tapestry-ui/tourimport { Tour } from '@tapestry-ui/tour';
import '@tapestry-ui/tour/styles.css'; // structural only — picks one colour, the dim
<Tour
open={touring}
onClose={() => setTouring(false)}
label="Dashboard tour"
steps={[
{ target: '.fuse', title: 'The countdown', body: 'Days until sell-ready.' },
{ target: '.property-z', title: 'Every property', body: 'A decade, month by month.' },
]}
/>Atomic parts
Split on Brad Frost's lines, like the accordion:
| | | |
|---|---|---|
| Spotlight | atom | dims the page, cuts a hole around one element |
| Coachmark | molecule | a spotlight plus the bubble that explains it — composed inside Tour |
| Tour | organism | the sequence, with the controls and the keyboard |
Spotlight and the pure helpers placeBubble / rectOf are exported, so you can
build a different sequence on the same foundation.
The two contracts
The component owns the SEQUENCE; the consumer owns the PAGE. A tour cannot
know that step four points at something inside a closed drawer. So a step may
carry onEnter — sync or async — that you use to put the page in the right
state. The tour then waits for the target to appear (up to 1.2s) before pointing
at it.
{ target: '#epic-editor', body: 'Epics live here.', onEnter: () => openDrawer('epics') }The container contract, and its exception. Tapestry UI components never own scrolling — but a tour must, because a step whose target is below the fold points at nothing. It scrolls the target, never a container of yours, and only when the target is actually off screen. A tour that jumps the page on every step is a tour nobody finishes.
A step whose target never arrives still speaks
It is not skipped and it does not throw. The bubble centres itself, the spotlight
stays dark, and onMissingTarget tells you — because a renamed class should cost
you one highlight, not the whole tour.
Props
| Prop | Type | Notes |
|---|---|---|
| steps | TourStep[] | { target?, title?, body, placement?, padding?, onEnter? } |
| open | boolean | |
| onClose | (reason) => void | 'finished' | 'skipped' | 'escaped' — they mean different things |
| index / onIndex | number / (i) => void | controlled; the tour reports intent and never drifts |
| label | string | names the tour when a step has no title |
| nextLabel backLabel skipLabel doneLabel | string | |
| showProgress | boolean | "3 of 7" (default true) |
| blocking | boolean | block the page outside the spotlight (default true) |
| onMissingTarget | (step, index) => void | |
| class / bubbleClass / id | string | skin hooks |
Keyboard
| Key | Does |
|---|---|
| → | next step, or finish on the last |
| ← | previous step |
| Esc | leave, reported as 'escaped' |
| Tab | cycles within the bubble while blocking |
Focus moves to the bubble on every step, so a keyboard and a screen reader follow
the tour instead of being left behind in the document. The bubble is a dialog
that does not claim aria-modal — the page behind it is the subject of the
tour, not scenery to be hidden — and the step count is announced from a polite
live region.
Styling
| Property | Default |
|---|---|
| --tui-tour-dim | rgb(0 0 0 / 0.6) |
| --tui-tour-hole-radius | 6px |
| --tui-tour-bubble-w | min(22rem, calc(100vw - 24px)) |
| --tui-tour-bubble-pad | 1rem |
| --tui-tour-gap | 0.5em |
| --tui-tour-focus | 2px solid currentColor |
| --tui-tour-z / --tui-tour-bubble-z | 60 / 61 |
| --tui-tour-move | the hole's transition; honours prefers-reduced-motion |
Parts: root (data-placement), spotlight (data-blocking), band, hole,
bubble, title, body, controls, progress, skip, back, next,
status.
How the hole works, in case you want to change it: the dim is a box-shadow
on the hole itself rather than a fill over the page, which is what lets the hole
have rounded corners. The four band elements around it are what actually catch
clicks — so the highlighted element stays usable while the rest of the page does
not. Never fork the package to restyle it.
License
MIT © Brandon Minton
