Plugins
Plugins add to a tour without forking it. A plugin is a plain object:
import type { TourPlugin } from "hintbeam";
const analytics: TourPlugin = {
name: "analytics",
onEvent(event) {
track(`tour_${event.type}`, { tour: event.tour.id, step: "index" in event ? event.index : undefined });
},
};
<TourProvider plugins={[analytics]} … />| Hook | Use it for |
|---|---|
name |
Required, unique. Shown in development warnings |
paths |
Add path styles: { zigzag: myStyle }, then path="zigzag" |
tours(context) |
Supply tours as data — a CMS, a file, a hosted editor. Each is validated first |
onEvent(event, context) |
Every start, step, complete, skip, stop — analytics, logging |
canStart({ tour, completed, user, platform }) |
Decide whether a tour may start — audiences, frequency caps, experiments, entitlements. Return false to block; start() then resolves to "blocked" |
setup(api) |
Drive tours: start, stop, add tours at runtime, list the targets on screen. Return a cleanup |
apiVersion |
The plugin API version you wrote against (currently 1) |
Rules that keep plugins safe:
- A failing plugin never breaks a tour. Errors in
onEventare reported and ignored; acanStartthat throws is treated as "allow". - Plugins add, they don't replace. They cannot change how targets are found, how steps end or what the card does.
- Every plugin must agree for a tour to start.
Examples
Show a tour only once:
const onlyOnce: TourPlugin = { name: "only-once", canStart: ({ completed }) => !completed };Only for new users:
const newUsers: TourPlugin = { name: "new-users", canStart: () => user.createdAt > Date.now() - 7 * 864e5 };Tours from anywhere
A plugin can supply tours as plain data — from a CMS, a JSON file, a hosted editor. Every tour is validated against your declared targets before it can play; invalid ones are reported and left out.
const cms: TourPlugin = {
name: "cms",
async tours({ user }) {
const response = await fetch(`/api/tours?locale=${user?.traits?.locale ?? "en"}`);
return response.json();
},
};
const { start } = useTour();
start("first-meal"); // by id, from any plugin or <TourProvider tours={[…]}>
const tours = useTours(); // everything that can be started, e.g. for a help menuKnowing who the user is
<TourProvider user={{ id: account.id, traits: { plan: account.plan, signedUpAt: account.createdAt } }} … />user is handed to canStart and onEvent. hintbeam itself never sends it anywhere.
Driving tours from a plugin
setup(api) runs when the provider mounts and may return a cleanup. api can start, stop,
getState, currentScreen, addTours, and list drawnTargets() — the named targets on screen
right now, with their boxes. That is everything a live preview or a visual editor needs:
const livePreview: TourPlugin = {
name: "live-preview",
setup(api) {
const socket = new WebSocket("wss://editor.example.com/preview");
socket.onmessage = (message) => {
const { added } = api.addTours([JSON.parse(message.data)]);
if (added[0]) void api.start(added[0]);
};
return () => socket.close();
},
};Metadata
Tours and steps accept meta — plain JSON up to 4 KB that travels with the tour and is never shown
to users. Use it for versions, experiments, audiences or editor notes.
Versioning
TOUR_PLUGIN_API_VERSION is the version of this contract (currently 1). Set apiVersion on your plugin;
the provider warns when a plugin needs a newer hintbeam. The contract only changes in
backwards-compatible ways within a major version.
Open source, and what may come later
Everything in this repository — every platform, router, path style and this plugin API — is MIT and stays MIT. The library never checks a licence key and never phones home.
Add-ons that need a server — hosted tour editing, live preview, analytics dashboards, audience targeting, cross-device progress — may be offered separately. They plug in through exactly the API on this page, so nothing in the open-source library is held back, and a free user never runs code they did not choose to install.