Overview
Element-level, cross-page product tour engine for AbsoluteJS apps.
@absolutejs/tourv0.3.0-beta.6betaFrontend & UXElement-level, cross-page product tour engine for Vue apps — a serializable step protocol, a pixel-accurate spotlight, and a controller that survives full-page navigations.
@absolutejs/tour is an element-level, cross-page product tour engine for Vue apps. Tours are described by a small serializable step protocol — target selector, route, placement, copy — so they can live in code, in a database, or be authored in an admin UI, and all render through the same engine. The spotlight stays pixel-accurate through late layout shifts, and a sessionStorage-backed controller resumes tours across full-page navigations in MPAs. Vue 3 and vue-router are peer dependencies; you render a tiny overlay component with your own styling.
bun add @absolutejs/tourElement-level, cross-page product tour engine for AbsoluteJS apps.
A tour is described by a small, serializable protocol (steps with a target selector, route, placement, and copy) so it can live in code, in a database, or be authored in an admin UI — and rendered by the same engine. The engine gives you:
Spotlight positioning — dims the page, highlights one real element, and
stays pixel-accurate. It re-measures on a light interval so the highlight follows late layout shifts (a page loading its data and reflowing after the first measure), not just scroll/resize.
Cross-page resume — in an MPA each page is its own mount, so moving between
steps is a full reload. The controller persists progress in sessionStorage and resumes on the next page at the right step.
A shared controller — start/stop the tour from anywhere (a first-visit
trigger, a "replay" button), all driving one overlay.
Vue 3 and vue-router are peer dependencies. The engine ships as composables; you render a tiny overlay component with your own styling (see below).
Add data-tour="…" attributes to the elements you want to spotlight. A step with no target (or one that can't be found) renders as a centered card.
Render an overlay component that consumes useSpotlight (style it however you like — the engine only computes geometry):
The host app owns: the data-tour anchors, the step content, and where the "seen" marker is stored (so finishing a first-visit run stamps it; a replay does not).
Steps stay serializable, so a step references actions by name; the host registers the handlers. onEnter actions run once the step is positioned (sequentially, cancelled if the step changes mid-run); onExit actions run when the step is left — cleanup/restore.
Built-ins (no host code needed): click, scroll, wait — each takes an optional selector (default: the step's target). Unknown action names warn and skip so a tutorial authored against an unmounted page degrades instead of breaking the tour. Handlers receive { step, target, args, index, signal, next, back, stop } — a handler can drive the tour itself (e.g. auto-advance when its demo finishes).
A tour must be able to show a data-backed surface (matches, pipeline) to a viewer who has no data yet — a fresh signup, an unsubscribed user — without the host paying to source anything. useTourDemo swaps in a constant, fully-typed sample dataset while the tour plays and passes the real data through untouched otherwise:
Resolution is per TourDataMode: "auto" (default) shows the viewer's real data when they have it — so a member with sourced matches is toured on their literal matches — and the sample when they don't; "demo" / "live" force one side (Tutorial.dataMode carries the choice in the serialized tutorial). Badge the surface when isDemo is true so sample data is never mistaken for real.
Pass onEvent to useSpotlight and every lifecycle moment lands in your analytics: tour_started, step_viewed, step_completed, step_target_missing, action_failed, tour_completed, and — the one that matters — tour_skipped, carrying the exact stepIndex, stepTitle, target, and route (the screen the viewer was on when they'd had enough) plus a reason distinguishing the Skip button from Escape.
Conditions mirror actions: serializable refs resolved by name against a registry (useTourConditions), with element and media built in.
Skipped steps are hopped in the direction of travel; if everything ahead is skipped the tour completes cleanly.
Below mobileQuery (default (max-width: 640px)) a step's mobile block overrides its target/placement/copy — or skips it where the anchor doesn't exist on small screens:
cta: { label: "Try it now", actions: [{ action: "click" }] } renders a button in the card (host template: v-if="step.cta" → @click="runCta"); it runs the refs through the action registry and advances unless advance: false.
useTourGate owns the auto-play decision across MANY tutorials: dismissal caps (trigger.maxDismissals, default 2 — after that, manual replay only), oncePerSession, priority when several tutorials match a page, audience showIf predicates, and role matching. State persists in localStorage.
Typed tasks + completion persistence + progress math; the host renders the panel. completeForTutorial(slug) checks off tasks tied to a tutorial (wire it to the tour_completed event).
Persistent pulsing beacons on tricky UI (independent of any tour) that open an explainer card on click. once: true hides a beacon after it's been opened; dismissals persist.
Dims the page and highlights one real element, re-measuring on a light interval so the highlight follows late layout shifts, not just scroll and resize.
The controller persists progress in sessionStorage, so a step that navigates to another page resumes at the right step after the full reload.
Steps reference actions and conditions by name; hosts register handlers, with click, scroll, wait, element, and media built in — so a tour can demo the product, not just point at it.
useTourDemo swaps in a fully-typed sample dataset while the tour plays, so fresh accounts can be toured on data-backed surfaces for free.
Every lifecycle moment — started, viewed, completed, skipped with the exact step and route — lands in your analytics via onEvent.
useTourGate handles dismissal caps, once-per-session, priority, and audience matching across many tutorials; checklist and hotspot engines round out onboarding.
Outcomes
Element-level, cross-page product tour engine for AbsoluteJS apps.
Add data-tour="…" attributes to the elements you want to spotlight. A step with no target (or one that can't be found) renders as a centered card.
Render an overlay component that consumes useSpotlight (style it however you like — the engine only computes geometry):
Hardening checklist
Follow in order
Conditions mirror actions: serializable refs resolved by name against a registry (useTourConditions), with element and media built in.
useTourConditions().register("hasDeals", () => dealCount.value > 0);Conditions mirror actions: serializable refs resolved by name against a registry (useTourConditions), with element and media built in.
{
title: "Your pipeline",
showIf: [{ condition: "hasDeals" }], // all must hold, else skipped
skipIf: [{ condition: "media", args: { query: "(max-width: 640px)" } }],
waitFor: { selector: ".pipeline-board", timeoutMs: 5000 }, // hold until ready
}Working example for The protocol.
import type { Tutorial } from "@absolutejs/tour";
const tour: Tutorial = {
slug: "portal-intro",
trigger: { firstVisitOnly: true, onRoutePrefix: "/portal" },
steps: [
{
title: "Welcome",
body: "A quick tour.",
placement: "center",
route: "/dashboard",
},
{
title: "Your command center",
body: "Your single best next step is always one click here.",
route: "/dashboard",
target: '[data-tour=\"hero\"]',
placement: "bottom",
},
// …intake, matches, network — each navigates and spotlights a real element
],
};Working example for Wiring it up.
// shared controller (singleton per storage key)
import { useTourController } from "@absolutejs/tour";
const tour = useTourController("myapp.tour");
// auto-play once on first visit, then stamp your own "seen" marker
if (!account.tourSeenAt) tour.start();
// replay from anywhere (does not re-stamp — pass replay=true)
tour.start(true);Tours are plain serializable objects — safe to store in a database or author in an admin UI. Add data-tour attributes to the elements you want to spotlight; a step with no target renders as a centered card.
import type { Tutorial } from '@absolutejs/tour';
const tour: Tutorial = {
slug: 'portal-intro',
steps: [
{
body: 'A quick tour.',
placement: 'center',
route: '/dashboard',
title: 'Welcome'
},
{
body: 'Your single best next step is always one click here.',
placement: 'bottom',
route: '/dashboard',
target: "[data-tour='hero']",
title: 'Your command center'
}
],
trigger: { firstVisitOnly: true, onRoutePrefix: '/portal' }
};Wire the shared controller and the spotlight engine; your overlay component consumes the computed styles and step state.
import { useSpotlight, useTourController } from '@absolutejs/tour';
import { PORTAL_TOUR_STEPS } from './steps';
// Shared controller (singleton per storage key)
const controller = useTourController('myapp.tour');
// Render your overlay from the computed geometry and state
const t = useSpotlight({
controller,
onClose: () => emit('close'),
onEvent: (event) => analytics.track(event),
steps: () => PORTAL_TOUR_STEPS
});
// t.step, t.spotlightStyle, t.tooltipStyle, t.next, t.back, t.skip
// Auto-play once on first visit, then stamp your own "seen" marker
if (!account.tourSeenAt) {
controller.start();
}
// Replay from anywhere (does not re-stamp)
controller.start(true);Steps reference actions by name so they stay serializable; unknown action names warn and skip instead of breaking the tour.
import { useTourActions } from '@absolutejs/tour';
// The page that OWNS the surface registers its demo handler
const actions = useTourActions();
const unregister = actions.register('matches.demo-swipe', async (ctx) => {
const direction = ctx.args.direction === 'left' ? 'left' : 'right';
await swiper.value?.demoSwipe(direction); // ctx.signal aborts long demos
});
onBeforeUnmount(unregister);
// The step invokes it — plain JSON, safe to store in a DB
const step = {
onEnter: [
{ action: 'matches.demo-swipe', args: { direction: 'right' } },
{ action: 'wait', args: { ms: 700 } },
{ action: 'matches.demo-swipe', args: { direction: 'left' } }
],
route: '/portal/matches',
target: "[data-tour='match-view']",
title: 'Swipe or list'
};Search the declarations exported by the current package type files. Expand a symbol to inspect its source-backed signature.
Current package surface
Import surface · click to copy