Skip to main content
Guided page tours are stepped balloons (“Step 1 of 4”) that walk a viewer round a page. Each tour is data: one *.tour.json per page, kept in the remote that owns the page and pointing at data-tour anchors in that remote’s markup. There is no per-page tour code. Two pieces work together:
  • The tour engine ships in @stralos-co-nz/ui from 0.76.0: the JSON schema, the registerTours registry, anchor helpers and the TourHost renderer.
  • The stralos-feature-tours widget remote (manifest key tours, slot floating-end, local preview port 4188) mounts one TourHost for the whole application and wires its adapters. See Micro-frontends.
Each remote bundles its own copy of @stralos-co-nz/ui, so the registry is plain data on window.__STRALOS_TOURS__ rather than a module singleton. registerTours writes to it and TourHost reads from it.

In a page remote

1. Declare anchors

Declare the anchors in src/tourAnchors.ts and put them on the elements the tour points at:
Shared blocks take the anchor as a prop: RegisterPage (tourAnchor, actionsTourAnchor, toolbarTourAnchor, search.tourAnchor), PageHeader (tourAnchor, actionsTourAnchor) and SideSheet (tourAnchor). Anchors use the form <area>.<thing> with lowercase kebab segments, for example rates.add-resource. Never reuse a data-testid or an aria-label as an anchor.

2. Write the tour JSON

Put the file at src/tours/<page>.tour.json:
id is persisted, so never rename it. Bump version to show a changed tour again to everyone who has already seen it. trigger.auto is first-visit or never.

3. Register the tour

Call registerTours once at module load with the remote’s tour files. Invalid tours are dropped with a console warning; nothing throws into the page.

Targeting a page with match

match.routeId is the shell’s route-catalogue id. The shell matches routes by prefix, so /rates/items/123 still reports rates. A tour with only routeId matches every pathname under that route. To pin a tour to one page, add match.path:
  • The tour matches only when both the route id and the pathname match.
  • path is an exact pathname, compared segment by segment. /rates matches /rates and /rates/ but not /rates/items/123.
  • A :param segment matches any one segment: /rates/items/:id matches /rates/items/123 but not /rates/items or /rates/items/123/edit.
  • Trailing slashes, the query and the hash are ignored. Wildcards, ?, # and empty segments are not allowed.
  • Moving to a pathname the tour’s path does not match ends the tour, as leaving the route does.

Step rules

Back and the close button are always offered and cannot be removed. Tours carry no scripts or programmatic clicks. TOUR_ALLOWED_EVENTS currently allows stralos:onboarding-open and stralos:user-settings-open; widening it is a reviewed edit to both schema.ts and tour.v1.schema.json in stralos-ui. Write tour copy in plain, friendly language. “Please” is fine. Never use “sorry”, “organisation” or “administrator”.

Empty pages

A tour usually runs on someone’s first visit, and for a new company that page is often empty: the first row a step points at does not exist yet. Give the step a fallback that points at the page’s empty state:
  • A fallback has the step’s content fields: type, anchor, placement, title, body and actions. Its type defaults the same way as a step’s.
  • Tooltip fallback: the step’s anchor and the fallback’s anchor are waited for at the same time, up to the step’s waitFor. Whichever shows first wins. When both are present, the step’s own anchor wins.
  • Dialog fallback: the step’s anchor is waited for, and the fallback shows centred if it never arrives.
  • onMissing applies only when neither shows.
  • Render the empty-state anchor only once the list has loaded and is empty. If it renders while the list is loading, the fallback can win before the first row arrives.
  • The fallback takes the step’s place: same id, same roles, same “Step n of N” position. It has no roles, waitFor or onMissing of its own.
  • Only a tooltip step can have a fallback. A dialog step always shows.

The tours widget remote

stralos-feature-tours holds no tours and no page knowledge. Its ./widget entry is mounted by the shell in the floating-end slot of the authenticated chromed layout. That entry is plain React and stays free of Chakra. The engine (TourHost with Chakra and Ark) is a lazy chunk that loads only once a remote has registered a tour or something dispatches stralos:tour-start.

Feature flag: guided-tours

The host is gated on the PostHog flag guided-tours, which is default on. Tours are hidden only when a flag request that succeeded evaluates guided-tours to false for the person. Every other case shows tours:
  • PostHog is off, not loaded, blocked or bot-filtered.
  • The flag request failed.
  • PostHog did not send the flag. An inactive flag is never sent to the browser, so it shows tours.
To hide tours for people, the flag must be active and evaluate to false for them, for example a release group at 0%. A local stack needs no override. The widget waits up to 1.5 seconds for PostHog’s first answer before mounting the host. A false that arrives later unmounts the host, which ends any open tour without saving progress. The remote reads the flag in src/toursFlag.ts rather than with the ui useFeatureFlag hook, because that hook reads a missing flag as false.

Adapters

Progress is stored per user and company. TourHost loads progress once per mount, so the widget keys it by ${organisationId}:${userId}.

Admin welcome gate

For an admin, tours are held back until the onboarding welcome dialog has settled for the current sign-in, so a tour does not start under a dialog that is about to cover it. The gate reads the onboarding remote’s own record, stralos.onboarding.welcome.<organisationId>.<userId> in local storage, without writing to it. Once the welcome has settled, tours wait a further 1.5 seconds so the dialog’s lazy chunk can load. The hold always ends 8 seconds after the page started waiting, because the onboarding remote may be absent or its read may fail. Members are never held.

Host behaviour

Starting by itself

Each page’s tour starts by itself on the first visit to that page. There is no per-sign-in cap: finishing the Rates tour and then opening Activities starts the Activities tour. A tour starts by itself when all of these hold:
  1. Its match.routeId, and match.path if set, match the current page.
  2. The viewer has no saved progress entry at the tour’s version or later.
  3. The viewport is at least minViewportPx wide (default 768 px).
  4. No modal layer is open and isBlocked() returns false.
  5. No other tour is running.
The page settles for 800 ms (autoStartDelayMs) before a tour starts. Within one visit to a page a tour never starts twice, even one that showed nothing. A new route id or pathname is a new visit.

Starting on demand

Dispatch stralos:tour-start on window:
Without a tourId, a tour with match.path is picked only when the current pathname matches it. On-demand start works even when the viewer has already seen the tour.

Progress

progress.save(tourId, { version, status }) is called with:
  • completed: the viewer finished the tour, used an end action, or followed a navigate or event action.
  • dismissed: the viewer closed the tour, pressed Escape, or left the page.
A tour that showed nothing saves nothing.

While a tour is open

  • <html data-stralos-tour-open="<tourId>"> is set.
  • stralos:tour-open-change fires on window when a tour opens and closes, so other floating widgets can step aside. Its detail is { open, tourId }, where tourId is null once the tour closes.
  • Read whether a tour is open, and its id, from the data-stralos-tour-open attribute or the event’s detail.tourId.

Layering

Tours render at TOUR_Z_INDEX (2100), above modals (1400), popovers and SideSheet (1500), toasts (1700), tooltips (1800) and the Combobox panel (2000). A step cannot point into a modal dialog or sheet, because the modal makes the balloon inert.

TourHost props

The tours widget remote is the only place that mounts TourHost. Its shape:

Schema and validation

The JSON Schema is exported as @stralos-co-nz/ui/tour.schema.json and as the TOUR_JSON_SCHEMA_V1 constant. Point an editor at the local copy with "$schema": "../../node_modules/@stralos-co-nz/ui/dist/tours/tour.v1.schema.json". validateTour applies the same rules at runtime, and registerTours runs it on every tour. In stralos-ui, src/tours/schema.test.ts keeps the TypeScript rules and the JSON Schema in step. Other exports include getRegisteredTours, TOURS_REGISTRY_KEY ("__STRALOS_TOURS__"), TOURS_REGISTERED_EVENT ("stralos:tours-registered"), tourAnchorProps, TOUR_ANCHOR_PATTERN, TOUR_ID_PATTERN, TOUR_ROLES, the TOUR_*_EVENT and TOUR_OPEN_ATTRIBUTE constants, and the Tour* schema and progress types.