Guide
A bidirectional in-app help system: a non-modal help panel, contextual hints, UI↔guide links, and an opt-in guided tour — all over one headless engine.
Playground
Billing
- Plan
- Pro — $29/mo
- Seats
- 5 of 10 used
<GuidePanel
title="Help"
>
<GuideArticle id="pg-plan" title="Billing & plans">
<p>
Your <GuideMention for="pg-plan">current plan</GuideMention> sets your monthly price and feature
limits. Upgrade or downgrade at any time.
</p>
<p>
Each <GuideMention for="pg-seats">seat</GuideMention> is one team member who can sign in.
You are billed per occupied seat — see
<GuideRef article="pg-seats">managing seats</GuideRef>.
</p>
</GuideArticle>
<!--
Der zweite Artikel ist das Ziel des `GuideRef` — und er macht die
`pg-seats`-Mention oben erst gültig, die vorher ins Leere zeigte.
`GuideRef` verlinkt *innerhalb* des Panels (Artikel → Artikel),
`GuideMention` verbindet die UI mit dem Panel: die zwei Richtungen,
die das Guide-System ausmachen.
-->
<GuideArticle id="pg-seats" title="Managing seats">
<p>
A seat frees up as soon as you remove a member. Billing follows on the next cycle, so
removing someone mid-month does not refund the current one.
</p>
<p>
Pricing per seat depends on your
<GuideRef article="pg-plan">billing plan</GuideRef>.
</p>
</GuideArticle>
</GuidePanel>01 Setup
Mount one GuideProvider near your app root. It instantiates a GuideController and shares it with every Guide surface via context. Mark any UI
element a topic with data-guide="<id>"; tours, hints, markers, and mentions
all resolve through that one namespace.
Provider + controller + a topic
<script lang="ts">
import { GuideProvider, Guide, GuideController } from '@urbicon-ui/blocks';
// Create the controller yourself for programmatic access (start tours, open the panel).
const guide = new GuideController();
</script>
<GuideProvider controller={guide}>
<!-- Any element becomes a guide target with a data-guide id -->
<button data-guide="save-button">Save</button>
<!-- Mount the tour renderer once; it stays invisible until a tour starts -->
<Guide />
</GuideProvider>
<button onclick={() => guide.startTour(welcomeTour)}>Take the tour</button>02 The help panel
The GuidePanel is a small help center living inside your product. Its index is
nothing more than the GuideArticles you mount — and it scales with them: bucket
articles into sections with group, switch on searchable when the
catalog grows, and cross-link related articles with GuideRef. All three demos below
open a real panel; close it with its × button or Escape.
Grouped sections
<GuideProvider controller={groupsGuide}>
<div
class="border-border-subtle bg-surface-elevated flex w-full max-w-md items-center justify-between gap-3 rounded-2xl border px-4 py-3"
>
<span class="text-text-primary text-sm font-semibold">Atlas — Projects</span>
<Button
variant="outlined"
intent="neutral"
size="sm"
onclick={() => groupsGuide.openPanel()}
>
Open help
</Button>
</div>
<GuidePanel title="Help">
<GuideArticle id="grp-first-project" title="Create a project" group="Getting started">
<p>A project collects everything one team ships: tasks, docs, and milestones.</p>
</GuideArticle>
<GuideArticle id="grp-invite" title="Invite your team" group="Getting started">
<p>Invite teammates by email — they join with access to every shared project.</p>
</GuideArticle>
<GuideArticle id="grp-plans" title="Plans & pricing" group="Billing">
<p>The Free plan covers three projects. Pro removes the limit.</p>
</GuideArticle>
<GuideArticle id="grp-seats" title="Seats" group="Billing">
<p>You are billed per occupied seat, prorated monthly.</p>
</GuideArticle>
<GuideArticle id="grp-shortcuts" title="Keyboard shortcuts">
<p>Press <strong>?</strong> anywhere to see the full shortcut map.</p>
</GuideArticle>
</GuidePanel>
</GuideProvider>Sections appear in the order their first article is defined; articles without a group (here: "Keyboard shortcuts") collect into one headerless block. When no article
sets a group at all, the index stays a flat list — grouping is purely opt-in.
Searchable index
<GuideProvider controller={searchGuide}>
<div
class="border-border-subtle bg-surface-elevated flex w-full max-w-md items-center justify-between gap-3 rounded-2xl border px-4 py-3"
>
<span class="text-text-primary text-sm font-semibold">Atlas — Settings</span>
<Button
variant="outlined"
intent="neutral"
size="sm"
onclick={() => searchGuide.openPanel()}
>
Open help
</Button>
</div>
<GuidePanel title="Help" searchable>
<GuideArticle id="srch-profile" title="Profile & avatar" group="Account">
<p>Your name and avatar appear on comments and shared views.</p>
</GuideArticle>
<GuideArticle id="srch-security" title="Password & security" group="Account">
<p>Change your password or add a passkey for phishing-resistant sign-in.</p>
</GuideArticle>
<GuideArticle id="srch-notifications" title="Notification preferences" group="Account">
<p>Choose which events reach you by email, push, or in-app.</p>
</GuideArticle>
<GuideArticle id="srch-import" title="Import from CSV" group="Data">
<p>Upload a CSV and map its columns to project fields.</p>
</GuideArticle>
<GuideArticle id="srch-export" title="Export your data" group="Data">
<p>Download the current view as CSV or JSON at any time.</p>
</GuideArticle>
<GuideArticle id="srch-scheduled" title="Scheduled exports" group="Data">
<p>Deliver a recurring export to email or webhook on a schedule.</p>
</GuideArticle>
</GuidePanel>
</GuideProvider>The filter matches article titles case-insensitively and runs before grouping, so empty sections disappear and an empty result announces itself politely. Closing the panel resets the query — a reopen starts from the complete index. Search pairs naturally with grouping, but works on a flat list just as well.
Cross-linked articles
<GuideProvider controller={refGuide}>
<div
class="border-border-subtle bg-surface-elevated flex w-full max-w-md items-center justify-between gap-3 rounded-2xl border px-4 py-3"
>
<span class="text-text-primary text-sm font-semibold">Trip to Lisbon — €412.80</span>
<Button
variant="outlined"
intent="neutral"
size="sm"
onclick={() => refGuide.openPanel('ref-pot')}
>
How is this split?
</Button>
</div>
<GuidePanel title="Help">
<GuideArticle id="ref-pot" title="The cost pot">
<p>
Every expense lands in the trip's shared pot. At the end, the pot is settled with as few
transfers as possible, based on each expense's
<GuideRef article="ref-splitting">splitting method</GuideRef>.
</p>
</GuideArticle>
<GuideArticle id="ref-splitting" title="Splitting methods">
<p>
Split equally, by shares, or by exact amounts. The method applies per expense and feeds
the <GuideRef article="ref-pot">cost pot</GuideRef>'s final balance.
</p>
</GuideArticle>
</GuidePanel>
</GuideProvider>A GuideRef navigates the open panel to another article — the help-internal analogue
of GuideMention, which links out to a UI element instead. It resolves against the
panel's article registry, so a ref pointing at an unknown id (or rendered outside a panel)
degrades to plain text instead of a dead link. The panel's back button returns to the index from
any article.
03 Contextual hints
A GuideHint waits at the right element instead of interrupting. Show it on mount,
or drive it from your own route/condition with trigger="manual" and open. It persists "seen" so it appears once, and steps aside while a modal or tour
is open.
A waiting hint
<GuideProvider controller={hintGuide}>
<div class="flex flex-wrap items-center gap-3">
<button
data-guide="ex-export"
class="border-border-default text-text-secondary rounded-lg border px-3 py-2 text-sm"
>
Export
</button>
<Button
variant="outlined"
intent="neutral"
size="sm"
onclick={() => (hintOpen = !hintOpen)}
>
{hintOpen ? 'Hide' : 'Show'} hint
</Button>
</div>
<GuideHint
for="ex-export"
trigger="manual"
open={hintOpen}
once={false}
title="New: scheduled exports"
onDismiss={() => (hintOpen = false)}
>
You can now export on a recurring schedule from here.
</GuideHint>
</GuideProvider>04 Guided tour & beacon
The guided tour is the deliberately opt-in, intrusive surface: a spotlight that dims everything
but the current step's target, plus an anchored bubble. A GuideBeacon is the gentle entry
point — a waiting hotspot that starts the tour on click, the opposite of an auto-start.
Beacon-launched tour
Dashboard
<GuideProvider controller={tourGuide}>
<div class="border-border-subtle bg-surface-elevated rounded-2xl border p-6">
<div class="mb-4 flex items-center justify-between">
<p class="text-text-primary text-sm font-semibold">Dashboard</p>
<span class="relative inline-flex">
<GuideBeacon tour={demoTour} once={false} />
</span>
</div>
<div class="flex flex-wrap gap-3">
<button
data-guide="tour-filters"
class="border-border-default text-text-secondary rounded-lg border px-3 py-2 text-sm"
>
Filters
</button>
<button
data-guide="tour-export"
class="border-border-default text-text-secondary rounded-lg border px-3 py-2 text-sm"
>
Export
</button>
</div>
<div class="mt-4">
<Button
variant="outlined"
intent="neutral"
size="sm"
onclick={() => tourGuide.startTour(demoTour)}
>
Start tour
</Button>
</div>
</div>
<Guide />
</GuideProvider>Tours survive client-side navigation: the controller lives in the layout's provider, an
unresolved target renders centered over the full scrim, and the bubble re-anchors as soon as
the new route's data-guide element appears. Give a step a route and
wire a navigate hook, and the library drives the navigation declaratively — going
to the step's route before the spotlight. A tour-internal navigation keeps the tour
running; a foreign one (the user leaving) stops it (analytics-silent). prev() navigates back symmetrically. Keep Guide mounted in the
layout (a route-local renderer unmounts on navigation and ends the tour). For routing chosen
at runtime, navigate imperatively in onStep instead (a tour with no route is never second-guessed).
Cross-route tour (declarative step.route + navigate hook)
<script lang="ts">
import { goto } from '$app/navigation';
import { GuideController, type GuideTour } from '@urbicon-ui/blocks';
// Wire the router once; the library stays framework-agnostic.
// (Equivalently: <GuideProvider navigate={(route) => goto(route)}>.)
const guide = new GuideController({ navigate: (route) => goto(route) });
const tour: GuideTour = {
id: 'cross-route-onboarding',
steps: [
{ target: 'dash-overview', route: '/dashboard', title: 'Your dashboard', body: '…' },
{ target: 'dash-filter', route: '/dashboard', title: 'Filter', body: '…' },
{ target: 'billing-plan', route: '/settings/billing', title: 'Your plan', body: '…' }
]
};
</script>05 Analytics
The real value of a tour is its funnel and drop-off signal. A GuideTour carries
three optional hooks fired from the engine, so they trigger no matter which surface drives the
tour. They are invoked defensively — a throwing handler never corrupts tour state. See GuideStepEvent / GuideEndEvent in the API reference for the payloads.
Wiring tour analytics
const welcomeTour: GuideTour = {
id: 'welcome',
steps: [
{ target: 'save-button', title: 'Save', body: 'Persist your changes here.' },
{ target: 'filter-control', title: 'Filter', body: 'Narrow the list.', interactive: true }
],
// Fired on start (via: 'start') and every next/prev — the step-by-step funnel.
onStep: ({ index, total, via }) =>
analytics.track('tour_step', { tour: 'welcome', step: index + 1, total, via }),
// Fired when the user finishes the whole tour.
onComplete: () => analytics.track('tour_complete', { tour: 'welcome' }),
// Fired when the user bails — event.index is where they dropped off.
onSkip: ({ index }) => analytics.track('tour_skip', { tour: 'welcome', droppedAt: index })
};06 The data-guide namespace
Every guide target is identified by a string id. There are two ways to register one — both feed the same registry, so a tour step, a hint, a marker, and a mention can all point at the same id.
Two ways to mark a target
<!-- 1. Declarative attribute — framework-agnostic, works on elements you don't render -->
<button data-guide="save-button">Save</button>
<!-- 2. Programmatic attachment — carries metadata (label, article, direction) -->
<button {@attach guide.target('save-button', {
label: 'Save button',
article: 'saving', // which panel article the marker opens
direction: 'both' // 'to-guide' | 'to-ui' | 'both' — gates Marker vs Mention
})}>Save</button>direction makes the bidirectional link a deliberate choice: 'to-ui' makes a GuideMarker inert (UI → guide off), 'to-guide' degrades a GuideMention to plain text (guide → UI off), and 'both' (the default) enables
both. In DEV, a target id referenced but not found in the DOM logs a warning instead of failing silently.
07 Accessibility
Keyboard parity
Markers and mentions are real <button>s. A mention highlights its target
on focus as well as hover, so the bidirectional link works without a mouse.
The tour bubble takes focus on open; → / ← step, Esc skips. An interactive step joins
its spotlit target to the bubble in one Tab cycle.
Announcements & focus
The tour announces each step through a polite aria-live region (so the
arrow-key path is never silent), and the hint announces itself with role="status". The tour returns focus to wherever it was when the tour ends.
Non-modal by design
The help panel has no focus trap and no backdrop — it coexists with the app (that is what
lets a mention highlight a field behind it). Escape only closes it while focus is inside, so
a foreground dialog keeps priority. Motion (panel slide, beacon pulse, step fade) honors prefers-reduced-motion.
08 Customization
Every surface supports unstyled, per-slot slotClasses, and named presets. Two tokens tune the tour's spotlight scrim and the additive highlight
ring.
Tokens & slot overrides
/* Tune globally via the design tokens (defaults shown — override to taste) */
:root {
--blocks-guide-scrim: oklch(0 0 0 / 0.5); /* the tour's dimming backdrop */
--blocks-guide-highlight-ring: var(--color-primary); /* the additive Mention→UI ring */
}
/* Or override per instance */
<GuidePanel slotClasses={{ panel: 'w-[28rem]', header: 'bg-surface-subtle' }} />
<GuideHint slotClasses={{ hint: 'max-w-sm' }} />09 API Reference
Guide (tour renderer)
Mount once inside GuideProvider; renders nothing until a tour starts.
Prop | Type | Default | Description | |
|---|---|---|---|---|
arrow | boolean | true | Render the bubble's pointer arrow on anchored steps. | |
class | string | — | Additional classes on the bubble. | |
padding | number | 8 | Padding in px between the step target and the spotlight hole edge. Also frames the additive highlight ring the engine paints on the target. | |
placement | Placement | 'bottom' | Preferred bubble placement when a step omits its own placement. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ Guide: {...} }}>. | |
radius | number | — | Spotlight hole corner radius in px. When omitted, it follows the target's own
border-radius (plus padding), clamped to the hole size. Set a number to force it. | |
slotClasses | Partial<Record<GuideTourSlots | 'skip' | 'prev' | 'next', string>> | — | Per-slot class overrides. Beyond the tour's own tv() slots, skip / prev / next
forward to the footer's nested <Button>s (which own their internal markup). | |
unstyled | boolean | false | Strip all default styles. |
GuideController
The headless engine. Create one and pass it to the provider for programmatic control.
Prop | Type | Default | Description | |
|---|---|---|---|---|
hasSeen(id) / markSeen(id) / resetSeen(id?) | (id: string) => boolean | void | — | Query, set, or clear the persisted "seen" state for a tour or hint id. | |
highlight(id, opts?) / clearHighlight() | (id: string, opts?: { scroll?: boolean }) => void | — | Add / remove the additive outline ring on a data-guide target. Shared by tour steps and Mention→UI. | |
new GuideController(options?) | GuideControllerOptions | — | Construct the engine. Options: storage (StorageAdapter, default localStorage), navigate ((route) => void | Promise — the cross-route hook, wire goto), navigationSource (current-path/navigation seam, default Navigation API + popstate), overlayStack, dev. Pass it to <GuideProvider controller={…}> for programmatic access. | |
next() / prev() | () => void | — | Advance / go back one step. next() on the last step completes the tour. | |
openPanel(article?) / closePanel() | (article?: string) => void | — | Open or close the help panel, optionally jumping to an article id. | |
skip() / finish() | () => void | — | End the tour (both mark it seen, unless once: false). skip fires onSkip, finish fires onComplete. | |
startTour(tour) | (tour: GuideTour) => boolean | — | Start a tour. Returns false if it was already seen (and once !== false) or has no steps. | |
stopTour() | () => void | — | Tear down a running tour WITHOUT marking it seen or firing analytics — for route changes / unmount. It can surface again later. | |
target(id, meta?) | (id, meta?: GuideTopicMeta) => Attachment | — | Svelte attachment that registers the host element as a guide target with optional metadata (label, article, direction). |
GuideTour
The tour definition you pass to startTour — lives in your app, not the library.
Prop | Type | Default | Description | |
|---|---|---|---|---|
id required | string | — | Unique id — used for "seen" persistence. | |
steps required | GuideStep[] | — | Ordered tour steps. | |
once | boolean | true | Skip automatically once completed or dismissed. | |
onComplete | (event: GuideEndEvent) => void | — | Fired when the tour completes (finish, or next past the last step). | |
onSkip | (event: GuideEndEvent) => void | — | Fired when the tour is dismissed before completing. event.index is the step the user dropped off at. | |
onStep | (event: GuideStepEvent) => void | — | Fired when a step becomes active — once on start (via: "start") and on each next/prev. Where the step-by-step funnel lives. |
GuideStep
Prop | Type | Default | Description | |
|---|---|---|---|---|
advance | 'user' | 'action' | 'user' | Learning-by-doing gate: with "action", Next/ArrowRight are inert (aria-disabled + screen-reader hint) and only controller.next() advances — call it once the user performed the real action. Usually paired with interactive: true. Back and Skip stay available. | |
body | string | — | Step body text. | |
interactive | boolean | false | Keep the spotlit target clickable through the scrim hole (and tabbable via the two-zone cycle). | |
placement | Placement | — | Preferred bubble placement relative to the target. | |
route | string | — | Route this step lives on (declarative cross-route touring). When set and ≠ the current location, the controller navigates there via the navigate hook before spotlighting, then re-anchors once the target appears. A tour-internal navigation keeps the tour running; a foreign one stops it. prev() navigates back symmetrically. Needs a navigate hook (else DEV-warns and stays put). | |
target | string | — | data-guide id to anchor to. Omit for a centered, full-scrim step. | |
title | string | — | Step heading. |
GuideStepEvent / GuideEndEvent
The payloads passed to the analytics hooks.
Prop | Type | Default | Description | |
|---|---|---|---|---|
index | number | — | Zero-based index of the active step. | |
step | GuideStep | null | — | The active step (GuideStep for onStep; nullable on the end events). | |
total | number | — | Total number of steps in the tour. | |
tour | GuideTour | — | The tour the event belongs to (handy for a shared, tour-keyed handler). | |
via | 'start' | 'next' | 'prev' | — | onStep only — how the step became active. |
GuideProvider
Context root — wires every surface to one GuideController.
Prop | Type | Default | Description | |
|---|---|---|---|---|
children required | Snippet | — | App subtree wired to the Guide context. | |
controller | GuideController | — | Supply a pre-created GuideController for programmatic access from outside the provider
(start tours, open the panel, query hasSeen). When omitted, the provider creates one
internally and shares it via context. When supplied, storage and navigate are ignored. | |
navigate | (route: string) => void | Promise<void> | undefined | Navigation hook for declarative cross-route tours, forwarded to the internally created
controller. A SvelteKit consumer wires (route) => goto(route); a step's route then
navigates before its spotlight. Ignored when a controller is supplied (set it there). | |
storage | GuideStorageAdapter | localStorage-backed adapter | Persistence adapter for "seen" state (tours/hints). |
GuidePanel
The callable, non-modal help panel (D1).
Prop | Type | Default | Description | |
|---|---|---|---|---|
children | Snippet | — | GuideArticle children and any custom content. | |
class | string | — | Additional classes on the panel root. | |
closeOnEscape | boolean | true | Close the panel when Escape is pressed. | |
footer | Snippet | — | Optional footer content. | |
id | string | auto-generated (`guide-panel-<id>`) | Stable DOM id for the panel root. GuideMarkers reference it via aria-controls. | |
placement | GuidePanelVariants['placement'] | 'right' | Side the panel docks to. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ GuidePanel: {...} }}>. | |
searchable | boolean | false | Render a filter input above the article index that matches article titles (case-insensitive). Off by default. Pairs with article grouping — filtered results keep their section headers and empty sections disappear. | |
size | GuidePanelVariants['size'] | 'md' | Size variant that controls dimensions and spacing of the GuidePanel | |
slotClasses | Partial<Record<GuidePanelSlots, string>> | — | Per-slot class overrides. | |
title | string | i18n `guide.openHelp` | Heading shown when no article is open. | |
unstyled | boolean | false | Strip all default styles. |
GuideArticle
A structured help article inside the panel.
Prop | Type | Default | Description | |
|---|---|---|---|---|
id required | string | — | Unique article id — referenced by GuideMarker and openPanel(id). | |
title required | string | — | Title shown in the panel list and header. | |
children | Snippet | — | Content to render inside the GuideArticle component | |
class | string | — | Additional classes on the article root. | |
group | string | — | Optional section this article belongs to in the panel index. Articles that
share a group are rendered under one section header (sections appear in
the order their first article is defined); articles without a group stay
in an ungrouped block. When no article sets a group, the index is a flat
list — unchanged from today. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ GuideArticle: {...} }}>. | |
slotClasses | Partial<Record<GuideArticleSlots, string>> | — | Per-slot class overrides. | |
unstyled | boolean | false | Strip all default styles. |
GuideMarker
Direction A — the discreet "ⓘ" trigger that opens the panel at an article.
Prop | Type | Default | Description | |
|---|---|---|---|---|
article | string | — | Article to open in the panel. Overrides the topic meta's article; falls back to for. | |
children | Snippet | — | Custom trigger content, replacing the default "ⓘ" icon. | |
class | string | — | Additional classes on the marker button. | |
direction | GuideDirection | the topic's `direction`, or `'both'` | Override the topic's link direction. The marker is live unless this resolves to 'to-ui'. | |
for | string | — | data-guide topic id this marker explains. Resolves the article (from topic meta) and
the link direction. Optional (unlike GuideMention.for) because article can stand
alone — supply one of the two. With neither, the marker opens the panel index. | |
label | string | i18n `guide.infoAbout` (with the topic's label) or `guide.info` | Accessible label for the icon button. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ GuideMarker: {...} }}>. | |
size | GuideMarkerVariants['size'] | 'md' | Size variant that controls dimensions and spacing of the GuideMarker | |
slotClasses | Partial<Record<GuideMarkerSlots, string>> | — | Per-slot class overrides. | |
unstyled | boolean | false | Strip all default styles. |
GuideMention
Direction B — inline article→UI reference that highlights the element.
Prop | Type | Default | Description | |
|---|---|---|---|---|
for required | string | — | data-guide id of the UI element to highlight. Required (unlike GuideMarker.for):
a mention with no target has nothing to highlight. | |
children | Snippet | — | Content to render inside the GuideMention component | |
class | string | — | Additional classes on the mention. | |
direction | GuideDirection | the topic's `direction`, or `'both'` | Override the topic's link direction. The mention is interactive unless this resolves to
'to-guide'. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ GuideMention: {...} }}>. | |
scroll | boolean | true | Scroll the target into view on click (reduced-motion-aware). | |
slotClasses | Partial<Record<GuideMentionSlots, string>> | — | Per-slot class overrides. | |
unstyled | boolean | false | Strip all default styles. |
GuideRef
Inline article→article link — navigates the panel to another article.
Prop | Type | Default | Description | |
|---|---|---|---|---|
article required | string | — | Id of the GuideArticle to navigate to. Inert (plain text) for an unknown id. | |
children | Snippet | — | Content to render inside the GuideRef component | |
class | string | — | Additional classes on the ref. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ GuideRef: {...} }}>. | |
slotClasses | Partial<Record<GuideRefSlots, string>> | — | Per-slot class overrides. | |
unstyled | boolean | false | Strip all default styles. |
GuideHint
Contextual, waiting hint anchored to a data-guide element.
Prop | Type | Default | Description | |
|---|---|---|---|---|
for required | string | — | data-guide id of the element to anchor to. Required — a hint with no anchor has nothing to point at. | |
arrow | boolean | true | Render the pointer arrow. | |
children | Snippet | — | Content to render inside the GuideHint component | |
class | string | — | Additional classes on the hint root. | |
once | boolean | true | When dismissed, persist a "seen" flag (via the controller's StorageAdapter) so the hint
does not reappear on later mounts. Mirrors the tour's "mark seen on end" rule — a hint
shown but never dismissed may show again. Set false to always show. | |
onDismiss | () => void | — | Called when the hint is dismissed (close button or Escape). | |
open | boolean | false | Manual visibility for trigger="manual" (the on-route / on-condition strategy). Ignored for
trigger="mount". Re-raising it to true after a dismiss re-surfaces the hint (subject to once). | |
placement | Placement | 'bottom' | Preferred placement relative to the target. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ GuideHint: {...} }}>. | |
seenId | string | the `for` id | Persistence key for the "seen once" state, decoupled from the anchor (this is *not* a DOM id — the popover gets none). Override only to track two hints on one element independently, or to keep a stable key across a renamed anchor. | |
slotClasses | Partial<Record<GuideHintSlots, string>> | — | Per-slot class overrides. | |
title | string | — | Optional bold heading above the body. | |
trigger | mountmanual | 'mount' | When the hint may appear: 'mount' shows it as soon as it mounts; 'manual' waits for
open to become true (the consumer's route/condition logic). | |
unstyled | boolean | false | Strip all default styles. |
GuideBeacon
Waiting, pulsing hotspot that starts an opt-in tour.
Prop | Type | Default | Description | |
|---|---|---|---|---|
class | string | — | Additional classes on the beacon button (use for absolute positioning over a target). | |
label | string | i18n `guide.startTour` | Accessible label for the button. | |
onActivate | () => void | — | Called on activation (click / Enter / Space), after tour is started when both are set. | |
once | boolean | true | Hide the beacon once its tour has been seen (and while that tour is running). Needs tour. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ GuideBeacon: {...} }}>. | |
size | GuideBeaconVariants['size'] | 'md' | Visual size of the hotspot. | |
slotClasses | Partial<Record<GuideBeaconSlots, string>> | — | Per-slot class overrides. | |
tour | GuideTour | — | The tour to start when the beacon is activated. When set, the beacon also hides itself once
the tour has been seen (subject to once). Omit to drive everything from onActivate. | |
unstyled | boolean | false | Strip all default styles. |
10 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
GuideProviderProps | interface | props | 0 | — | |
GuidePanelProps | interface | props | 0 | — | |
GuideArticleProps | interface | props | 0 | — | |
GuideMarkerProps | interface | props | 0 | — | |
GuideMentionProps | interface | props | 0 | — | |
GuideRefProps | interface | props | 0 | — | |
GuideHintProps | interface | props | 0 | — | |
GuideProps | interface | props | 0 | — | |
GuideBeaconProps | interface | props | 0 | — | |
GuidePanelVariants | type | variant | 0 | — | |
GuideTourVariants | type | variant | 0 | — | |
GuideBeaconVariants | type | variant | 0 | — | |
GuideArticleVariants | type | variant | 0 | — | |
GuideMarkerVariants | type | variant | 0 | — | |
GuideMentionVariants | type | variant | 0 | — | |
GuideRefVariants | type | variant | 0 | — | |
GuideHintVariants | type | variant | 0 | — | |
GuidePanelSlots | type | variant | 0 | Slot names derived from each tv() config — single source of truth for slotClasses. | |
GuideTourSlots | type | variant | 0 | — | |
GuideBeaconSlots | type | variant | 0 | — | |
GuideArticleSlots | type | variant | 0 | — | |
GuideMarkerSlots | type | variant | 0 | — | |
GuideMentionSlots | type | variant | 0 | — | |
GuideRefSlots | type | variant | 0 | — | |
GuideHintSlots | type | variant | 0 | — | |
GuideController | class | helper | 0 | Headless engine for the Guide system — the UI-free state machine behind every
Guide surface (Panel, Marker, Mention, Hint, Tour).
One instance per GuideProvider (Phase 2) — *not* a singleton, so multiple scopes
can coexist and tests get a fresh instance. Modeled on OverlayStack (class with
$state + untrack).
Responsibilities:
- **Target registry** — maps data-guide ids to live DOM elements, fed by the
target() attachment and resolved with a [data-guide="…"] DOM fallback.
- **Tour state machine** — startTour / next / prev / skip / finish.
- **Highlight** — highlight / clearHighlight, shared by tour steps and the
bidirectional Mention→UI link (Direction B). Toggles a data-guide-highlight
attribute; the additive ring itself is pure CSS (token-driven, D5).
- **Panel state** — openPanel / closePanel (the UI lands in Phase 3).
- **Persistence** — hasSeen / markSeen via an injectable GuideStorageAdapter.
- **Analytics hooks** — fires the active tour's onStep / onComplete / onSkip
callbacks (the actual business value of a tour) defensively, so a throwing
consumer callback can never corrupt tour state or leak the overlay entry.
- **overlay-stack integration** — a running tour registers itself so it pauses
when a foreign modal (Dialog/Drawer) stacks on top. | |
GuideDirection | type | helper | 0 | Which link directions a topic supports (D3/§4). both when omitted. | |
GuideStorageAdapter | interface | helper | 0 | Persistence boundary for "seen" ids. The default is localStorage-backed, but a consumer can inject a server-state adapter. Kept deliberately tiny. | |
GuideTour | interface | helper | 0 | A guided tour definition. | |
Placement | type | helper | 1 | — | |
GuideControllerOptions | interface | helper | 0 | Options for GuideController; every dependency is injectable for testing. | |
GuideTopicMeta | interface | helper | 0 | Metadata attached to a registered target via target(id, meta). | |
GuideStep | interface | helper | 0 | A single step of a guided tour. | |
GuideStepEvent | interface | helper | 0 | Payload for GuideTour.onStep — fired when a step becomes the active one. | |
GuideEndEvent | interface | helper | 0 | Payload for GuideTour.onComplete and GuideTour.onSkip — a snapshot of
where the tour ended (e.g. the step a user skipped from, for drop-off analytics). | |
Side | type | helper | 0 | — | |
Alignment | type | helper | 0 | — | |
GuideOverlayStackLike | interface | helper | 0 | Minimal slice of the overlay stack the controller depends on (injectable for tests). | |
GuideNavigationSource | interface | helper | 0 | Source of the current route and navigation notifications the controller needs for cross-route
tours (injectable for tests and custom routers). The default createBrowserNavigationSource
reads window.location.pathname and subscribes via the Navigation API, falling back to popstate. |
11 Installation
Import
import {
GuideProvider,
GuidePanel,
GuideArticle,
GuideMarker,
GuideMention,
GuideRef,
GuideHint,
Guide,
GuideBeacon,
GuideController
} from '@urbicon-ui/blocks';
import type { GuideTour, GuideStep, GuideStepEvent, GuideEndEvent } from '@urbicon-ui/blocks';