Hotspots
A marker on one element that opens a short explanation and can start a tour, what retires it for good, and when it draws nothing at all.
A hotspot is a small marker pinned to one element, outside any tour. Clicking it opens a bubble
with a title, a body, and, optionally, a button that starts a tour. Opening the bubble marks the
hotspot seen, for good: a seen hotspot draws no marker again, on this page or the next visit, until
reset() is called.
Hotspot and ResolvedHotspot
interface Hotspot {
id: string;
target: string;
title?: string;
titleKey?: string;
body?: string;
bodyKey?: string;
tourId?: string;
placement?: 'top' | 'bottom' | 'left' | 'right';
}
target: the logical key carried by the element’sdata-guideattribute, matched the same way a tour step’stargetis.title/body: literal text.titleKey/bodyKeyresolve through the provider’stranslateinstead, exactly as a tour step does.tourId: the tour started when the bubble’s “Show me” button is clicked. Omit it and the bubble shows only a Close button.placement: overrides the default bubble placement for this hotspot only.
Two hotspots sharing an id make HotspotProvider throw at render, immediately, the same as a
duplicate tour id.
useHotspots() hands back each hotspot resolved to a ResolvedHotspot, with its text already
translated and its seen state attached:
interface ResolvedHotspot {
id: string;
target: string;
title: string;
body: string;
seen: boolean;
tourId?: string;
placement?: 'top' | 'bottom' | 'left' | 'right';
}
HotspotProvider and useHotspots
import {
HotspotProvider,
createBrowserStorage,
type Hotspot,
} from '@apollovisionlabs/guide-core';
import { Hotspots } from '@apollovisionlabs/guide-mui';
const storage = createBrowserStorage('my-app');
const hotspots: Hotspot[] = [
{ id: 'share', target: 'project.share', title: 'Share a project', tourId: 'sharing' },
];
export function App() {
return (
<HotspotProvider hotspots={hotspots} storage={storage}>
<AppRoutes />
<Hotspots />
</HotspotProvider>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
hotspots |
Hotspot[] |
none | The hotspots available in this tree. Ids must be unique. |
children |
ReactNode |
none | Your application. |
storage |
GuideStorage |
none | Persists which hotspots have been opened, under the single key hotspots:seen. See Persistence. |
translate |
(key: string) => string |
none | Resolves titleKey / bodyKey. |
onEvent |
(event: GuideEvent) => void |
none | Called for hotspot:show and hotspot:open. See “Events” below. |
useHotspots() returns:
interface UseHotspotsResult {
hotspots: ResolvedHotspot[];
restored: boolean;
open: (hotspotId: string) => void;
startTour: (hotspotId: string) => void;
reset: () => void;
notifyShown: (hotspotId: string) => void;
}
hotspots: every hotspot, seen and unseen alike, each carrying its ownseen. A renderer needs the seen ones too, to keep a marker mounted while its own bubble closes; filtering to the unseen ones is one line at the call site if that is all you want.restored: whether the initial read from storage has settled.trueimmediately when nostorageprop was given, since there is nothing to wait for, andtrueonce the read resolves or rejects otherwise, including a failed read. A renderer should wait for this before drawing any marker, or a hotspot already seen in storage can flash on screen once before the restore lands.open(hotspotId): marks the hotspot seen and emitshotspot:open. Calling it again on a hotspot already seen does nothing further. Logs a warning and does nothing for an id that names no hotspot.startTour(hotspotId): starts the hotspot’stourIdon the nearestGuideProvider. Does nothing if the hotspot has notourId. Warns once if the tree has noGuideProvider, and warns once if the tour fails to start.reset(): clears every seen hotspot at once, bringing every marker back. There is no per-hotspot undo.notifyShown(hotspotId): called by a renderer once a marker is actually drawn on screen. Driveshotspot:show, and is deduplicated per hotspot per mount, so a scroll that re-measures the target does not announce a second impression.Hotspotsalready calls this for you; write it yourself only if you render your own markers on top ofuseHotspots.
Opening retires a hotspot, permanently
open(hotspotId), which the shipped marker calls when it is clicked, is the only thing that sets
seen to true. Once it has run for a hotspot, that hotspot’s ResolvedHotspot.seen is true
everywhere useHotspots() is read, and, with storage configured, it stays true across reloads
because it is written under hotspots:seen. The Hotspots component renders nothing for a seen
hotspot. The only way back is reset(), and it takes every hotspot with it: there is no way to
un-see one hotspot on its own.
The MUI Hotspots component
import { Hotspots } from '@apollovisionlabs/guide-mui';
<Hotspots labels={{ startTour: 'Show me', close: 'Close' }} placement="bottom" />;
| Prop | Type | Default | Description |
|---|---|---|---|
labels |
Partial<HotspotLabels> |
see below | The marker’s accessible name and the bubble’s button text. |
placement |
'top' | 'bottom' | 'left' | 'right' |
'bottom' |
Default bubble placement; a hotspot’s own placement wins. |
zIndex |
number |
theme.zIndex.drawer + 1 |
Stacking level of the marker; the bubble sits one above it. See “Limits” below for why this can matter. |
HotspotLabels.marker is a function
interface HotspotLabels {
marker: (title: string) => string;
startTour: string;
close: string;
}
Defaults: marker: (title) => \Show what is new: ${title}`, startTour: ‘Show me’, close: ‘Close’`.
marker builds the marker button’s accessible name, and it is a function rather than a template
string because where the title falls in the sentence is not the same from one language to the
next: a translation may need to put the title first, wrap it differently, or drop the leading
phrase altogether. startTour and close are plain strings, the same as the rest of labels on
GuideTour and Checklist.
Events
| Event | Payload | When |
|---|---|---|
hotspot:show |
{ hotspotId } |
A hotspot’s marker is actually drawn on screen. Emitted once per hotspot per mount. |
hotspot:open |
{ hotspotId } |
The hotspot’s bubble is opened, which also marks it seen. Not emitted again for a bubble that is already open; a second click on an open marker just closes it. |
Hotspots defer to a running tour
While a tour is running or paused, Hotspots draws no markers at all, not only the ones that
collide with the current step’s target. A marker sits position: fixed over its target’s
top-right corner, so when a tour step points at that same element, the marker can intercept the
click meant to advance an advanceOn: 'click' step, sit pulsing uselessly on top of a
non-interactive step’s spotlight, or hold keyboard focus over the step that a tour just launched
from its own bubble. Rather than resolve each of those collisions in place, no hotspot marker
draws while a tour is live.
paused counts as live: a paused tour is waiting for its target, not finished. This is
suppression, not retirement. Nothing here touches seen, so markers come back exactly as they
were, unseen ones included, the moment the tour stops or completes, and hotspot:show fires again
for them since its deduplication is per mount, not global.
Limits
- A hotspot inside a modal dialog is covered. The marker’s default
zIndexistheme.zIndex.drawer + 1, chosen to sit above an app bar or a drawer, where a hotspot’s target often is, and below a running tour’s spotlight attheme.zIndex.modal. A dialog also renders above the drawer layer, so a hotspot whose target lives inside one is covered by the dialog unlesszIndexis raised past it. - With
storageconfigured, no marker draws until the initial read settles.Hotspotswaits forrestoredbefore rendering anything, so on a slow storage backend the first paint has no markers at all, and they appear once the read resolves. - A tour paused on a target that never mounts suppresses every hotspot for as long as it stays
paused. Under the
waitmissing-target policy, a paused tour already draws nothing itself; with hotspots deferring to it too, the page can end up with no visible affordance at all,Escapebeing the only way out.
Related
- Tours for
tourId, the missing-target policies, and whatpausedmeans. - Persistence for
GuideStorageand the merge behaviour on restore. - Migrating to 0.3.0 for what changed when hotspots were added.
- API reference for every exported symbol.