Guidebeta
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>
<!--
The second article is the target of the `GuideRef`, and it makes the
`pg-seats` mention above resolve (before it pointed at nothing).
`GuideRef` links *within* the panel (article → article); `GuideMention`
connects the UI to the panel: the two directions the Guide system is built on.
-->
<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 and give it a GuideController. Every Guide component inside then finds that controller
automatically. Mark any UI element as 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>02 The help panel
The GuidePanel is a small help center inside your product. Its index is 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.
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 shows an empty state. Closing the panel resets the query; a reopen starts from the complete index. Search works with grouping or on a flat list.
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. It is the panel-internal
counterpart to GuideMention, which links out to a UI element. A ref pointing at an
unknown id, or one 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 anchors to an element and shows a short message there. By default (trigger="mount") it shows when it mounts; trigger="manual" hands control to the open prop
for your own route or condition. It persists "seen", so it appears only once, and hides while a modal
or tour is open.
A waiting hint
New: scheduled exports
<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 most intrusive of them: a spotlight that dims everything but the current
step's target, plus an anchored bubble. A GuideBeacon is a pulsing hotspot that starts
the tour when clicked.
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 triggers no automatic navigation).
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
A tour reports its funnel and drop-off through three optional hooks on GuideTour,
which fire no matter which component drives the tour. A throwing handler is caught, so it cannot
stop the tour. 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>Both directions read the same id. A GuideMarker next to a topic opens the panel at
its article (UI → guide); a GuideMention inside that article highlights the topic's element
on hover or focus (guide → UI). The Playground above wires both live.
A marker and a mention
<GuideProvider {controller}>
<!-- UI to guide: the marker opens the panel at its article -->
<h3>Billing <GuideMarker for="plan" article="billing" /></h3>
<div data-guide="plan">Pro plan</div>
<GuidePanel title="Help">
<GuideArticle id="billing" title="Billing & plans">
<!-- guide to UI: hovering or focusing the mention highlights the marked element -->
<p>Your <GuideMention for="plan">current plan</GuideMention> sets the price.</p>
</GuideArticle>
</GuidePanel>
</GuideProvider>direction controls which way the link works: '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.
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, so it coexists with the app: a mention can
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 Guide component 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
9 partsGuide (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 Guide component 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' | Panel width. | |
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 | — | Article body. | |
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' | Icon size. | |
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 | — | The mention text. | |
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 | — | The link text. | |
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 | — | Hint body. | |
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 component (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';