Overview
A guided tour is an interactive overlay on your own running app — it spotlights the next element and waits for the user to actually click or type it on their own account. Unlike an embedded demo, which plays back screenshots inside a player, a tour drives your real UI.
A tour is its own asset with a text-only source format: a tours/<key>.tour.json file you keep in your repo, next to the UI it targets. A recording is the scaffold, not the source — it contributes the selectors and drift anchors once, then the file is yours to edit, review, and version like any other code.
Runs on your app
Spotlights real elements on your live page — not a screenshot player.
Lives in git
A plain JSON file with a published schema — diffable, reviewable, validated in CI.
Show once
Auto-runs once per visitor by default — ideal for first-run onboarding.
Survives UI drift
Falls back to a step's recorded text when its selector changes.
Author a tour
Scaffold from a recording (the CLI projects the recorded selectors, copy, and fallback anchors into a starting file), or start blank:
stepshots tour init onboarding --from output/onboarding.stepshot
# or, from scratch:
stepshots tour init onboardingNo CLI recording handy? The browser extension can download a tour file straight from a recording made in your real, logged-in app.
Either way you get tours/onboarding.tour.json — with editor autocomplete and validation via its $schema entry:
{
"$schema": "https://raw.githubusercontent.com/hauju/stepshots/main/schema/tour.schema.json",
"schema": "1",
"key": "onboarding",
"title": "Getting started",
"steps": [
{
"selector": "#new-project",
"title": "Create your first project",
"body": "Everything starts with a project — click here.",
"advance": { "type": "click" },
"fallback": { "text": "New project" }
}
]
}Each step spotlights selector, shows title/body in a callout, and advances on the user's real interaction (click, input, or change). The optional fallback anchors let the player find the element by its text or aria-label when the selector drifts.
Three things make a good tour:
- Record against a fresh account. Tours run for brand-new users on empty accounts — record the flow in the state your users will be in.
- Keep it to the activation path. Get the user to their first real win in a handful of steps. Long tours get skipped.
- Write callouts as instructions. Demo captions describe ("See how easy it is to…"); tour callouts tell the user what to do ("Click New project").
Tag a tutorial with "target": "tour" in stepshots.config.json and stepshots record warns about interactive steps without a callout (they'd be dropped from the tour) and scaffolds the tour file for you after recording.
Validate it — including in CI
# Static: strict schema check + lints, CI-friendly exit codes
stepshots tour validate
# Live: replay the tour headless against a real deploy
stepshots tour check --url https://staging.example.comtour check resolves each step exactly like the player does — selector first, then the fallback anchors — performs the step's action, and moves on. Three outcomes per step: ok (selector matched), drift (only a fallback anchor matched — fix the selector soon), fail (nothing matched). Run it in CI and selector drift is caught before your users meet it.
# Refresh the text/aria fallback anchors from the live DOM
stepshots tour check --url https://staging.example.com --update-fallbacksServe it
Hosted (recommended): push the tour and paste one script tag. Hosting is free with unlimited tours; pushing again overwrites the hosted copy — your git file stays the source of truth.
stepshots tour push<script
src="https://stepshots.com/tour.js"
data-stepshots-tour="your-tour-id"
defer
></script>On the free plan, hosted tours show a small "Powered by Stepshots" link at the bottom of the callout card; Pro removes it.
Self-hosted: no account needed. Import the .tour.json directly with your bundler and call the @stepshots/tour player yourself, or emit a registry script:
stepshots tour build -o public/tours.jsAdd the snippet to the page(s) where the flow starts — for example, your empty-state dashboard for a "create your first project" tour.
Localize a tour
A tour is a text file in your repo, so translating it is a pull request, not a dashboard project. A translated variant sits next to its base file, shares its key, and declares its language:
stepshots tour init onboarding --locale de
# → tours/onboarding.de.tour.json, copied from tours/onboarding.tour.jsonThe scaffold copies the base file's structure — selectors, advance rules, check hints — so the only thing to change is the strings: each step's title and body, and the file's title. Then build one registry per language and load the one matching your app's UI language, exactly how the rest of your frontend does i18n:
stepshots tour build -o public/tours.js # default locale
stepshots tour build --locale de -o public/tours.de.jsA localized build prefers each tour's de variant and falls back to the base for tours you haven't translated yet — shipping partially translated is fine. Tour keys stay identical across languages, so ?tour= links, show-me triggers, and checklist items need no per-locale changes.
Two things keep translations honest:
tour validatefails CI when a translation goes stale. A variant must track its base's structure; when the base gains or loses a step, validation errors on the out-of-sync variant — you find out in the pull request, not from a German user seeing last quarter's onboarding.- Fallback anchors are per-language. The drift anchors are the target's visible text — "New project" in English, "Neues Projekt" in German — so refresh each variant's anchors from the deploy it will run on:
stepshots tour check --url <localized-staging> --update-fallbacks tours/onboarding.de.tour.json.
Hosted tours currently serve the default locale — tour push skips locale variants; serve localized registries yourself with tour build --locale.
Answer your FAQ with tours
Tours aren't only for first-run onboarding — any element can become a "show me" launcher. Mark it with data-stepshots-tour-trigger and clicking it starts that tour, fresh from step 0:
<details>
<summary>How do I invite a teammate?</summary>
<p>Members live under Settings → Team.</p>
<button data-stepshots-tour-trigger="your-tour-id">Show me</button>
</details>With hosted tours the value is the tour id from stepshots tour push (one tour.js tag serves any number of triggers). Self-hosted, it's the tour key from your window.__STEPSHOTS_TOURS registry. Triggers are delegated, so FAQ items rendered later — accordions, SPA views — work without extra wiring. A clicked trigger counts as a real run in your analytics; use ?tour= links to preview without counting.
When the flow starts on a different page than the FAQ, add data-stepshots-tour-url: the click navigates there first, and the tour starts on arrival (install the snippet globally so the destination page has it too):
<button
data-stepshots-tour-trigger="your-tour-id"
data-stepshots-tour-url="/settings/team"
>Show me</button>This turns a help center into self-serve support: instead of describing the flow, the answer walks the user through it in their own account. See examples/faq-show-me.html for a complete page.
Ship an onboarding checklist
Bundle your activation tours into a persistent "Getting started · 2/5" launcher: a corner chip that expands into a checklist, where each item runs a tour and checks off when the user completes it. Progress persists per browser, and once everything is done the panel offers a dismissal that's remembered.
With hosted tours it's declarative — list your tour ids on the same script tag:
<script
src="https://stepshots.com/tour.js"
defer
data-stepshots-checklist='[
{"tour":"first-tour-id","label":"Create your first project"},
{"tour":"second-tour-id","label":"Invite a teammate","url":"/settings/team"}
]'
data-stepshots-checklist-title="Getting started"
></script>Give an item a url when its flow starts on a different page: clicking navigates there first and the tour starts on arrival — so install the snippet globally, the same rule as jump triggers. Checklist runs count as real usage in each tour's analytics funnel. On the free plan the panel and its tours carry the "Powered by Stepshots" link; Pro removes it — same mechanic as everywhere else, decided server-side per response.
Self-hosting instead? The player package exports the same widget as createChecklist — you pass the items and your registry directly.
Converting an existing demo
Already have a demo whose steps carry callouts? Open it, choose Embed → Guided tour → Convert to guided tour. This materializes the demo's projected tour (including any copy overrides) into a standalone tour you can manage like any other — the demo itself is unchanged. Demo-hosted tours that were enabled before tours became their own asset keep working unchanged.
Options
Set these as attributes on the <script> tag:
| Attribute | Default | Description |
|---|---|---|
data-stepshots-tour |
required | The tour id to run. |
data-once |
true |
Show the tour once per browser (remembered in localStorage, marked once the tour actually starts). Set "false" to run on every load. |
data-when |
— | A CSS selector that gates first-run: the tour waits until that element exists before starting (e.g. your empty state). Until then it doesn't run and isn't marked as seen. |
data-accent |
— | Spotlight ring color, any CSS color (e.g. #3b82f6) — match your brand. Also themes the checklist. |
data-stepshots-checklist |
— | JSON array of {"tour","label","url"?} items — mounts an onboarding checklist over those hosted tours. |
data-stepshots-checklist-title |
Getting started |
Heading for the checklist launcher and panel. |
data-stepshots-checklist-position |
bottom-right |
Corner for the checklist launcher; set bottom-left when a chat widget owns the right corner. |
For onboarding, pair a global snippet with data-when pointed at your empty-state element (e.g. data-when="[data-empty-projects]"). A returning user who lands on a populated page won't consume their one-time run — the tour waits for the state it was authored against.
When a tour doesn't run, the loader logs why under the [stepshots-tour] prefix in the browser console (no tour steps, already shown, or a failed fetch) — check there first while wiring it up.
Force a run for testing
Append ?tour=<tour-id> (or ?tour=1) to any URL to run the tour immediately, ignoring the once-per-browser flag. Handy for previewing and for shareable "take the tour" links:
These links also make great support macros — a reply link that walks the user through the fix in their own account. See Answer Support Tickets with Tour Links.
How advancing works
The tour listens for the real interaction on the highlighted element:
- Click steps advance when the user clicks the target — and the click still does its normal job (following a link, opening a dialog, and so on).
- Typing steps advance once the user types a non-empty value into the target field.
- Select steps advance the moment the user picks an option from the highlighted dropdown (on the field's
changeevent).
The overlay never blocks your app: it observes the interaction and moves on. When a step's target mounts after a client-side (SPA) transition, the tour waits for it and keeps flowing. It also survives a full page reload: the current step is saved to sessionStorage, so the tour resumes right where it left off — classic multi-page (server-rendered) flows work in the same tab too. Ending the tour (finishing or skipping) clears the saved step.
The user can dismiss the tour at any time — by clicking Skip tour on the card or pressing Escape. The callout is also announced to screen readers (it's a labelled dialog with a live region, so each step's title, body, and progress are read aloud as the tour advances), and it honors the OS reduce-motion setting.
Surviving UI changes
Each tour step carries the target's recorded text and aria-label alongside its CSS selector. If the selector stops matching because your markup drifted, the player falls back to finding the element by those anchors — so a small refactor won't silently break onboarding.
Run stepshots tour check --url <staging> in CI to catch drift before deploying, and --update-fallbacks to refresh the anchors from the live DOM — no re-recording needed. When the flow itself changes, edit the tour file (or re-scaffold with tour init --from --force) and push.
Tour analytics
Every tour run reports anonymous, aggregate progress back to Stepshots. All plans see the topline: tours started and completion rate. Pro adds the per-step funnel — how far each run reached, where users skipped — and drift alerts: steps where runs repeatedly got lost (the target never appeared) are flagged, a signal that your UI changed underneath the tour.
Only counts are stored, aggregated per tour. No personal data, IP addresses, or typed input is collected — the analytics can't identify individual users, and the values people type into your fields never leave their browser. Your own previews via ?tour= links are excluded so they don't pollute the numbers.
Tour vs. embedded demo
| Guided tour | Embedded demo | |
|---|---|---|
| Runs on | Your live app | A player (screenshots) |
| Best for | Onboarding, product walkthroughs | Marketing, docs, landing pages |
| User action | Real clicks/typing on your UI | Watch / step through |
| Source of truth | A .tour.json file in your repo |
A recorded .stepshot bundle |
| Where it lives | Your own pages | Anywhere (site, blog, iframe) |
| Snippet | tour.js + data-stepshots-tour |
embed.js + data-stepshots-demo |