Version v0.1.0 Size 3.6 KB gzip
Why Beacon?
Product tours rot fast: hand-wired step sequences hard-code positioning, persistence, and target waiting into application code, and break the first time the UI moves. Beacon is the headless half of a tour — it owns step order, target waiting, resumable progress, and events — while your markup (or @vielzeug/refine's ore-tour components) decides what a step looks like.
// Before: step logic tangled with rendering and storage
let step = Number(localStorage.getItem('tour-step') ?? 0);
function showStep() {
const el = document.querySelector(steps[step].selector);
card.style.top = `${el.getBoundingClientRect().bottom + 8}px`; // breaks on scroll
card.innerHTML = steps[step].html;
}
// After: the engine owns order, persistence, and the anchor rect
import { createTour } from '@vielzeug/beacon';
const tour = createTour({ id: 'welcome', steps, storage });
tour.current.subscribe(() => renderCard(tour.current.value)); // rect is the live anchor
tour.start();| Feature | Beacon | Shepherd.js | Intro.js |
|---|---|---|---|
| Bundle size | 3.6 KB | ~50 kB | ~25 kB |
| External dependencies | |||
| Headless (you own the UI) | |||
| Resumable persisted progress | |||
| Versioned re-offers | |||
| Coachmarks (single hints) |
Use Beacon when tours must survive UI changes: steps anchor to your real DOM, progress persists and resumes, and the card markup is yours (or refine's ready-made ore-tour).
Consider Shepherd or Intro.js when you want a fully-rendered, opinionated tour widget you never style yourself and can accept their DOM and CSS.
Installation
pnpm add @vielzeug/beaconnpm install @vielzeug/beaconyarn add @vielzeug/beaconQuick Start
Define the tour as data, run it, and render the current step wherever you like. The handle is disposable, so component teardown cleans everything up.
import { createTour, type BeaconTourConfig } from '@vielzeug/beacon';
const config = {
id: 'getting-started',
steps: [
{ id: 'welcome', title: 'Welcome', description: 'A two-minute look around.' },
{ id: 'nav', target: '[data-tour="nav"]', title: 'Navigation', placement: 'bottom-start' },
],
} satisfies BeaconTourConfig;
const tour = createTour(config);
// Render from the live snapshot: rect is the anchor, null centers the card.
const unsubscribe = tour.current.subscribe(() => {
const { presentation, rect, step } = tour.current.value;
console.log({ presentation, rect, step }); // drive your card from this
});
tour.start();
// On teardown:
unsubscribe();
tour.dispose();Features
createTour()runs a sequential, resumable walkthrough over your own DOM.createCoachmark()shows a single contextual hint once per user per version.currentis a liveReadable<BeaconCurrentStep>snapshot: step, index, placement, presentation, and resolved anchor rect, kept fresh on scroll, resize, and layout changes.targetaccepts a selector,Element, rect, or lazy resolver, and beacon waits for lazily-mounted targets before showing the step.storagepersists progress through anyBeaconStoragebridge (localStorage, IndexedDB via@vielzeug/vault, a server API).versionre-offers a finished tour when the product changes underneath it.tap()subscribes to lifecycle events (step:start,tour:complete,target-missing, ...) withAbortSignalsupport.resolveTarget()turns any target into a viewport rect compatible with@vielzeug/orbitpositioners.ore-tour'sbind()in@vielzeug/refinerenders a handle and routes its actions with zero glue code.
Documentation
See Also
- Refine: ships
ore-tourandore-spotlight, the ready-made dialog/popover/bottom-sheet presentation layer for a beacon handle. - Orbit: compute exact popover coordinates from
current.rectwhen you build a custom presentation. - Vault: durable storage backends that plug into
BeaconStoragewith a six-line bridge. - Ripple: the
Readablesignals every beacon handle exposes come from here.