Getting Started
Problem
You want a first guided tour: a few steps anchored to your app's own buttons, a card that follows them, progress that survives a reload, and no tour framework dictating your markup. The APIs involved are createTour, BeaconStorage, and refine's ore-tour element.
Solution
Define the tour as data and hand the handle to card.bind() — one call renders the current snapshot into the card and routes the card's actions back into the handle.
ts
import { createTour, type BeaconTourConfig, type BeaconStorage } from '@vielzeug/beacon';
import type { OreTourElement } from '@vielzeug/refine/tour';
const storage: BeaconStorage = {
get: (key) => JSON.parse(localStorage.getItem(key) ?? 'null'),
remove: (key) => localStorage.removeItem(key),
set: (key, value) => localStorage.setItem(key, JSON.stringify(value)),
};
const config = {
id: 'first-tour',
presentation: 'auto',
steps: [
{ description: 'Two minutes, five stops.', id: 'welcome', presentation: 'modal', title: 'Welcome' },
{ id: 'nav', placement: 'bottom-start', target: '[data-tour="nav"]', title: 'Get around' },
{ id: 'share', placement: 'bottom-end', target: '[data-tour="share"]', title: 'Share your work' },
],
storage,
} satisfies BeaconTourConfig;
const tour = createTour(config);
const card = document.querySelector<OreTourElement>('#tour-card')!;
// Renders `current` into the card and wires next/back/skip/finish/dismiss back.
// Beacon keeps the anchor rect fresh on scroll, resize, and layout changes, so no listeners here.
const unbind = card.bind(tour);
tour.start(); // no-op if this user already completed the tour
// On teardown: unbind(); tour.dispose();The matching markup is one element plus your target hooks:
html
<ore-tour id="tour-card" spotlight></ore-tour>
<nav data-tour="nav">…</nav>
<button data-tour="share">Share</button>With a Replay control
ts
function replay(): void {
tour.reset(); // clears the persisted record
tour.start();
}Pitfalls
- Setting
card.stepIndex(camelCase) does nothing: ore exposes props under their literal kebab-case keys (card['step-index']).bind()writes them for you. current.subscribe()listeners take no arguments — readtour.current.valueinside the listener; this is ripple'sReadable, not an observable that passes values.- A step's
targetselector that never mounts holds the tour open forever under the defaulttargetTimeoutMs: Infinity; set a finite timeout and watchtap()fortarget-missing. start()after a completed tour is a silent no-op by design — callreset()first for replays.