# Guided tours

> **Section:** [Guides](https://transcodeworks.github.io/guides.md)
> **Related:** [Annotations](https://transcodeworks.github.io/guides/annotations.md) · [The authoring GUI](https://transcodeworks.github.io/guides/authoring.md) · [Configuration](https://transcodeworks.github.io/guides/configuration.md) · [Next.js and other hosts](https://transcodeworks.github.io/guides/hosts.md) · [Agents (MCP)](https://transcodeworks.github.io/guides/mcp.md) · [Scenes and shots](https://transcodeworks.github.io/guides/scenes.md)
> **Also:** [HTML version](https://transcodeworks.github.io/guides/tours) · [Docs index](https://transcodeworks.github.io/llms.txt)

---

A tour follows the same idea as a screenshot: define it against the real app, in
the repo, and it can't quietly rot. Steps anchor to elements with the same
`data-shot` selectors your shots use, live in a `.tour.ts` next to the feature
they tour, and ship in the same PR as the UI change that would otherwise break
them.

Two packages split the work:

- **`@stillsmith/tour`** is the production runtime: small, framework-neutral, two
  dependencies. This is what your app installs and ships.
- **`@stillsmith/capture`** is the dev dependency you already have. It authors tours,
  providing the editor's tours mode, the codemod that writes your `.tour.ts` back,
  and the MCP tools for agents.

## Declaring a tour

```ts
// src/tours/onboarding.tour.ts
import type { Tour } from "@stillsmith/tour";

export const Onboarding = {
  id: "onboarding",
  steps: [
    { title: "Welcome", body: "A quick look around." },
    { target: { selector: "[data-shot='search']" }, body: "Find anything from here." },
    {
      target: { selector: "[data-shot='save']" },
      body: "Go ahead and click it.",
      advance: { on: "click" },
    },
    { route: "/settings", target: { selector: "[data-shot='theme']" }, body: "Make it yours." },
  ],
} satisfies Tour;
```

Named exports are tours, the same way named exports of a scene file are shots.
Targets resolve `selector` → `text` → `rect`, same as annotations, and the same
rule applies: a `data-shot` attribute is the anchor that survives.

## Running it

```tsx
import { TourAutoStart } from "@stillsmith/tour/react";
import { Onboarding } from "@/tours/onboarding.tour";

<TourAutoStart tour={Onboarding} />
```

Or imperatively, with `startTour(Onboarding)`. Progress persists in
`localStorage`: a completed or dismissed tour stays quiet, and an interrupted one
resumes where it left off. Esc dismisses, arrow keys step, and focus moves into
the card and back out at the end.

The runtime covers the awkward parts:

- Targets that mount late are polled for per-step. `optional: true` skips one that
  never shows; a required miss ends the tour with a console warning and no
  persisted verdict, so it tries again next visit.
- The spotlight is an SVG cutout that passes clicks through to the real control,
  which is what makes `advance: { on: "click" }` work.
- Tooltips flip and shift at viewport edges, and a step's `offset` is applied last
  and always wins.

<Aside type="note">
A step with `route` navigates there first, through a three-function
`RouterAdapter`. The default drives `history.pushState` and dispatches
`popstate`; if your router keeps its own state store, pass an adapter wired
to it in `TourOptions`.
</Aside>

## Fixtures: data the tour needs

A tour walks your real app, so a feature with nothing in it has nothing to point
at. A brand-new account shows an empty list, and the step that spotlights a row
waits eight seconds and gives up. Name a fixture and the tour brings its own data:

```ts
export const Onboarding = {
  id: "onboarding",
  fixture: "demo-rocks",
  steps: [/* … */],
} satisfies Tour;
```

The name is a string, not a function, for the same reason `body` is: it
round-trips through the authoring codemod. What it means lives in your app,
registered once at startup:

```ts
import { registerTourFixtures } from "@stillsmith/tour";

registerTourFixtures({
  "demo-rocks": {
    setup() {
      shelfStore.add(DEMO_ROCKS);
      return () => shelfStore.remove(DEMO_ROCKS.map((r) => r.id));
    },
  },
});
```

`setup` runs before the first step; the cleanup it returns runs when the tour
ends, whichever exit it takes — completed, dismissed, or torn down. Return
nothing and provide a `teardown` instead if that reads better; a returned
cleanup wins if you do both. Either may be async, and the engine waits for
`setup` before showing step one.

Register at module scope. A tour can start before React has mounted anything, so
a fixture registered inside an effect is often too late.

Three rules worth knowing:

- **Setup must be idempotent.** A tour the user abandoned resumes on their next
  visit and seeds again, over state that may or may not still hold the first
  seed. Add by id rather than appending blindly.
- **An unregistered or failing fixture ends the tour** with a console warning and
  no persisted verdict — so a misconfigured deploy doesn't burn the tour's one
  shot, and it tries again next visit. Same rule as a missing required target.
- **Teardown isn't transactional.** If the user closes the tab mid-tour, nothing
  runs. Prefer client-side or clearly-flagged demo data over records you'd have
  to reach a server to delete.

For a controller wired by hand, `TourOptions.fixtures` passes handlers directly
and takes precedence over the registry:

```ts
startTour(Onboarding, { fixtures: { "demo-rocks": { setup } } });
```

The authoring GUI and the MCP `preview_step` / `inspect_app` tools apply the
fixture too, so elements that only exist once data is present are targetable
while you author. In the GUI the seed stays until you reload the frame or switch
tours; a play-through seeds and cleans up on its own.

## Authoring visually

Add the globs to your config:

```ts
export default defineConfig({
  scenes: ["src/**/*.scene.tsx"],
  tours: ["src/tours/*.tour.ts"],
  // …
});
```

`stillsmith dev` then grows a tours mode next to shots. Its stage doesn't render a
scene; it iframes your running app, with a route bar. Click an element to target
it (the same suggestion engine grades how stable the selector is), drag the card
to nudge its `offset`, or press play to run the real tour in the frame.
Rehearsals run with persistence off, so they never mark the real tour completed.

Save writes your `.tour.ts` back through the same codemod that writes shots: only
the `steps` and `fixture` properties are touched, minimal diff, your formatter.

By default the stage serves your app through stillsmith's merged Vite server. If
your app runs its own dev server — a Next.js app under `next dev`, say — point
stillsmith at it instead:

```ts
appUrl: "http://localhost:3000",
```

The stage then iframes that origin directly, no build integration at all. Note
that a cross-origin `appUrl` disables live step previews in the GUI (the
browser forbids reaching into the frame); the MCP `preview_step` tool renders
through Playwright and still works.

## Agents

The MCP server gains the tour counterparts of its scene tools: `list_tours`
(what exists), `inspect_app` (what's targetable on a route, with stability
grades), and `preview_step` (render one step, saved or proposed inline, over the
live app and return the PNG). The injected runtime is resolved from your
`@stillsmith/tour` install, so what the agent sees is the version you ship.

Both `inspect_app` and `preview_step` seed the tour's fixture first, so an agent
writing a step against seeded data sees the same page your users will.