AbsoluteJS

Checklist, Hotspots & the Overlay

One guided run is not an onboarding system. useTourChecklist keeps a persistent getting-started panel — typed tasks, completion persistence, progress math — useTourHotspots puts always-on beacons on tricky UI, and useSpotlight turns the serialized steps into pixel-accurate geometry for an overlay you render and style yourself.

#Three Help Surfaces

The three surfaces split the same way: the composable owns discovery, state, persistence, and geometry; the host renders and styles. They also share one storageKey namespace (default 'absolute.tour'), so a product's tour, checklist, and hotspots persist side by side.

SurfaceSpotlight tourChecklistHotspots
ShapeLinear steps across pagesTask list with progressPer-element beacons
LifetimeOne run per triggerUntil dismissedAlways on
PersistsProgress in sessionStorageCompletions in localStorageDismissals in localStorage
Best forFirst-visit walkthroughActivation milestonesTricky UI explainers

#Getting-started Checklist

The stickiest pattern in this category: a persistent panel with N tasks, a progress bar, and each task launching a tour or a deep link. useTourChecklist is the logic only — the host renders the panel and decides what launching a task means. State lives in localStorage under storageKey.checklist.id, so completions survive sessions.

TS
import { useTourChecklist } from '@absolutejs/tour';

const checklist = useTourChecklist({
	// Identity of this checklist — its persistence bucket in
	// localStorage (namespaced by storageKey, default 'absolute.tour').
	id: 'onboarding',
	// The tasks — a getter so the list can be reactive.
	items: () => [
		{ href: '/portal/intake', id: 'intake', title: 'Finish your intake' },
		// Completing this tutorial checks the task off (see
		// completeForTutorial below).
		{ id: 'tour', title: 'Take the tour', tutorialSlug: 'portal-intro' },
		{
			description: 'Get your first three matches reviewed.',
			href: '/portal/matches',
			id: 'matches',
			title: 'Review your matches'
		}
	]
});

// The host renders the panel:
// checklist.items.value    → [{ ...item, done }]
// checklist.progress.value → { done, total, percent }

Each TourChecklistItem is plain data:

idIdentity of the task — completions persist against it.
title / description?The copy the host renders in the panel row.
tutorialSlugA tutorial this task launches — completing that tutorial (via completeForTutorial) checks the task off.
hrefOr a plain deep link the host navigates to.

#Completion & Progress

Tasks complete three ways: the host calls complete(id) directly, the viewer finishes a tutorial tied to the task, or the host re-derives completion from product state. The tutorial path is the interesting one — the engine's funnel events carry the tutorialSlug, so one line in the onEvent sink closes the loop.

TS
// Wire tutorial completion into the checklist from the engine's funnel
// events — finishing 'portal-intro' checks off every task tied to it.
useSpotlight({
	controller,
	onClose,
	onEvent: (event) => {
		if (event.name === 'tour_completed') {
			checklist.completeForTutorial(event.tutorialSlug ?? '');
		}
	},
	steps: () => PORTAL_TOUR_STEPS,
	tutorialSlug: () => 'portal-intro'
});

// Manual task management:
checklist.complete('intake');
checklist.uncomplete('intake');
checklist.isDone('intake'); // boolean

// Dismissal persists (an ISO timestamp) until restored:
checklist.dismiss(); // checklist.dismissed.valuetrue
checklist.restore();

// Wipe completions AND the dismissal:
checklist.reset();
itemsComputed [{ ...item, done }] — the panel maps straight over it.
progress{ done, total, percent } — percent is rounded, and 0 for an empty list. The progress bar comes for free.
complete / uncomplete / isDoneManual task management by item id.
completeForTutorial(slug)Checks off every task tied to that tutorial — wire it to the tour_completed funnel event.
dismissed / dismiss() / restore()Dismissal persists (an ISO timestamp) until restored — a closed panel stays closed across sessions.
reset()Wipes completions AND the dismissal.

The activation loop end to end:

1
hostRender the panel from items and progress
The composable owns the logic — typed items, completion persistence, progress math — and the host owns the panel.
checklist.items.value → [{ ...item, done }]
2
hostA task launches a tutorial or a deep link
A tutorialSlug task starts its tour through the shared controller; an href task is a plain navigation.
3
engineThe tour plays and reports
useSpotlight emits tour_completed when the viewer finishes the last step.
4
hostSink the completion into the checklist
Every task tied to that tutorial flips to done — the funnel event is the bridge.
tour_completed → checklist.completeForTutorial(slug)
5
hostProgress updates, dismissal persists
progress recomputes on its own; when the viewer is finished with the panel, dismiss() hides it for good.

#Always-on Hotspots

Some UI needs explaining every time, not once — a trust score, a weekly refresh cadence. useTourHotspots keeps persistent pulsing beacons on those elements, independent of any linear tour, and opens an explainer card on click. Same machinery as the tour's beacon and card, but per-element and permanent.

TS
import { useTourController, useTourHotspots } from '@absolutejs/tour';

const tourController = useTourController('myapp.tour');

// Persistent pulsing beacons on tricky UI — independent of any linear
// tour — that open an explainer card on click.
const spots = useTourHotspots({
	// Master switch: hide beacons while a tour is playing. Default on.
	enabled: () => !tourController.active.value,
	// The hotspots for the current page — a getter so it can be reactive.
	hotspots: () => [
		{
			body: 'How aligned this investor is with your goals.',
			id: 'trust-fit',
			placement: 'right',
			target: "[data-tour='trust-fit']",
			title: 'Trust & Fit'
		},
		{
			body: 'Matches refresh every Monday morning.',
			id: 'refresh',
			// Hide the beacon permanently once it has been opened.
			once: true,
			target: "[data-tour='refresh']",
			title: 'Weekly refresh'
		}
	]
});

// spots.beacons.value → [{ hotspot, style }] — one beacon per target
// spots.card.value    → { hotspot, style } | null — the open card
// spots.open(id) / spots.close() / spots.dismiss(id) / spots.restoreAll()
idIdentity of the hotspot — dismissals persist against it.
targetCSS selector for the element the beacon rides.
title / bodyThe copy of the explainer card.
placementdefault "bottom"Preferred card side. bottom flips to top when the viewport runs out of room, and the card clamps on screen.
mediaOptional image or video shown in the card.
onceHide the beacon permanently once the viewer has opened it.
Beacons off while a tour plays
The enabled getter is the master switch (default on). Wire it to !tourController.active.value so pulsing beacons never compete with an active spotlight.

#Beacons & Card

The composable measures every eligible target on scroll, resize, and a light 250ms interval, and hands the host ready-to-render position styles. A target with no visible box (a collapsed drawer, a hidden panel) is skipped rather than beaconed at 0×0.

beaconsOne { hotspot, style } per visible target. Elements that vanish (page changed, panel closed) simply drop out — beacons follow the DOM.
card{ hotspot, style } for the open explainer, or null — its position is clamped to the viewport.
open(id) / openIdOpen a hotspot’s card; openId is the currently open id.
close()Close the card — and permanently dismiss the hotspot when it is marked once.
dismiss(id)Persist a dismissal (localStorage) and close the card if it is open.
restoreAll()Clear every persisted dismissal.
Dismissals persist
dismiss(id) and a once hotspot's first open both persist to localStorage — the beacon stays gone across sessions until restoreAll(). Useful for a "restore tips" switch in settings.

#Rendering the Overlay

useSpotlight is the engine behind everything the other pages configure: it walks the steps, navigates cross-page, runs actions, evaluates branches, and emits funnel events — but it renders nothing. The host teleports a small overlay to body and styles it however it likes; the engine only computes geometry.

VUE
<script setup lang="ts">
import { useSpotlight, useTourController } from '@absolutejs/tour';
import { PORTAL_TOUR_STEPS } from './steps';

const controller = useTourController('myapp.tour');
const emit = defineEmits<{ close: [] }>();

const t = useSpotlight({
	controller,
	// Below this query, steps' mobile blocks win. Default shown.
	mobileQuery: '(max-width: 640px)',
	onClose: () => emit('close'),
	// Hold positioning (and navigation!) until the host has resolved
	// WHICH steps to play — an async tutorial fetch on a cross-page
	// resume. The engine re-locates when this flips true.
	ready: () => tutorialLoaded.value,
	steps: () => PORTAL_TOUR_STEPS,
	// Per-tutorial theme → CSS custom properties on the overlay.
	theme: () => activeTutorial.value?.theme
});
</script>

<template>
	<Teleport to="body">
		<div
			v-if="t.active.value && t.step.value"
			class="tour-root"
			:style="t.themeVars.value"
		>
			<div class="tour-blocker" :class="{ dim: t.isCentered.value }"></div>
			<div class="tour-spotlight" :style="t.spotlightStyle.value"></div>
			<div
				class="tour-tooltip"
				:class="{ centered: t.isCentered.value }"
				:style="t.isCentered.value ? {} : t.tooltipStyle.value"
			>
				<p>Step {{ t.index.value + 1 }} of {{ t.stepCount.value }}</p>
				<h3>{{ t.step.value.title }}</h3>
				<p>{{ t.step.value.body }}</p>
				<button @click="t.skip">Skip</button>
				<button v-if="!t.isFirst.value" @click="t.back">Back</button>
				<button @click="t.next">
					{{ t.isLast.value ? 'Done' : 'Next' }}
				</button>
			</div>
		</div>
	</Teleport>
</template>
controllerThe shared switch from useTourController — the overlay consumes what start() flips.
stepsGetter for the ordered steps. Swapped under a running tour (an async tutorial fetch), the engine re-positions against the new list.
onCloseCalled when the viewer finishes the last step or skips.
actions / conditionsRegistries that resolve the steps’ serialized action and condition names. Default to the shared keyed singletons.
mobileQuerydefault "(max-width: 640px)"Below this query, steps’ mobile blocks win — target, placement, copy, or skip.
onEvent / tutorialSlugThe funnel-event sink and the tutorial identity stamped on every event.
readyHold positioning (and navigation!) until the host has resolved WHICH steps to play — the engine re-locates when it flips true.
themePer-tutorial TourTheme, exposed to the host as CSS custom properties via themeVars.
Keyboard comes wired
While the tour is active, Escape skips (reason "escape"), ArrowRight / Enter advance, and ArrowLeft goes back — no host wiring needed.

#Overlay Surface

Positioning works from the card's measured size: the engine flips to the roomier side when the preferred placement can't fit, then clamps both axes so the card — and its Skip / Next controls — can never leave the viewport. After each step it re-measures on short settle delays and a light 150ms interval, so the highlight follows late layout shifts; a target that collapses mid-step degrades to the centered card instead of a 0×0 spotlight.

active / step / index / stepCountWhere the viewer is. step is the resolved current step, mobile overrides already applied.
isFirst / isLastThe edges — hide Back on the first step, relabel Next to "Done" on the last.
isCenteredTrue when there is no target rect — welcome and closing steps, and steps whose target went missing, render as a centered card.
next / back / skipNavigation. skip(reason) emits tour_skipped — pass "escape"-style strings; non-strings coerce to "skip".
runCta()Runs the step’s cta action refs through the registry, then advances unless advance: false.
spotlightStylePosition and size for the highlight ring, honoring the step’s spotlight padding, radius, and shape.
tooltipStyle / tooltipElThe card position, computed from the card’s MEASURED size — bind ref="tooltipEl" so clamping never works from a guess.
blockersThe click-blocking layers: one full-screen rect, or four bands around the target when spotlight.allowInteraction keeps the element clickable.
showBeacon / beaconStyleThe pulsing hint dot for beacon: true steps — a lighter touch than the full spotlight.
cardAnimationStyleEntrance animation per the step’s transition — re-key the card per step so it replays.
themeVars / isMobileCSS custom properties from the tutorial theme, and whether the mobile query currently matches.