Recipes

A target that renders after a request

The default gate already waits for the target to appear, bounded by gateTimeoutMs.

{ id: "chart", target: "revenue-chart", gateTimeoutMs: 10000 }

If the wait is about your data rather than the element, say so:

{
  id: "chart",
  target: "revenue-chart",
  gate: async ({ context }) => context.reportsLoaded,
  gateTimeoutMs: 10000,
}

A target inside a modal

On web this works as long as the modal is in the document when the step activates. Open it first:

{ id: "settings", target: "#modal-save", onEnter: () => openSettings() }

onEnter is awaited before the gate runs, so the modal is mounting while the tour waits for the target.

The overlay sits at theme.zIndex, 10000 by default, so it renders above a modal rather than behind it. Lower it if you want the reverse.

Showing a tour only once

Completion is persisted for you under tour:{id}:v{version}.

import { readRecord } from "@tourkit/core";

const record = await readRecord(storage, onboarding);
if (record?.outcome !== "completed") start("onboarding");

Resuming where the user left off

Automatic. start(id) resumes if a saved position exists and the step still resolves, and emits tour:resume instead of tour:start.

Bump version when you change the steps and the saved position is discarded rather than resumed onto the wrong step.

Re-running a finished tour

import { clearRecord } from "@tourkit/core";

await clearRecord(storage, onboarding);
start("onboarding");

Two tours, one after the other

<TourProvider
  tours={[onboarding, advanced]}
  onEvent={(name, event) => {
    if (name === "tour:complete" && event.tourId === "onboarding") start("advanced");
  }}
>

A step that only some users see

{ id: "billing", target: "billing", when: (context) => context.plan === "pro" }

Hidden steps are excluded from total, so progress stays honest.

Pointing at a list row

Give the row a stable target id and let the tour scroll to it.

{items.map((item, index) =>
  index === 3 ? (
    <TourTarget key={item.id} id="third-row">
      <Row item={item} />
    </TourTarget>
  ) : (
    <Row key={item.id} item={item} />
  ),
)}
{ id: "row", target: "third-row", scroll: { block: "start" } }

On web the same thing is a data-tour-id on the row.

Stopping a tour when the user navigates away

const { stop, running } = useTour();

useEffect(() => {
  if (running && pathname !== expected) stop();
}, [pathname, running, stop, expected]);

Usually unnecessary, since a step with a route navigates back on its own.

Different copy per locale

title and body are plain strings, so translate them where you build the config.

const onboarding = (t: (key: string) => string): TourConfig<Ctx> => ({
  id: "onboarding",
  version: 1,
  steps: [{ id: "post", target: "post-ride", title: t("tour.post.title"), body: t("tour.post.body") }],
});

Pass the result to tours. Changing it mid-tour is safe.

Finding a typo in a target id

A step whose target never appears is skipped after gateTimeoutMs and emits target:timeout. Log it in development:

<TourProvider
  tours={tours}
  onEvent={(name, event) => {
    if (name === "target:timeout") console.warn("tourkit: no target for step", event.stepId);
  }}
>