How to build a product tour that survives your next deploy

Learn how to build a product tour that guides users to activation, attaches to real UI, survives route changes, and stays editable after the next deploy.

How to build a product tour that survives your next deploy

Ship weekly for a quarter and your product tour goes stale roughly thirteen times. One week the anchor breaks, the next the label changes, then the pointer ends up floating over empty space. Every one of those misses costs time to track down before it costs you a user.

The real question is not how to build a product tour. It's whether the tour you build will survive the next deploy. This guide is about keeping it short, contextual, and tied to one activation moment, while making sure a UI change doesn't turn it into a support ticket.

Use a product tour only when it gets someone to activation

The tour vs tooltip vs checklist decision

A product tour makes sense when the user needs to do something they can't figure out from context alone, and that thing takes more than one step across more than one UI element. A single tooltip works when there's one unfamiliar element and no sequence. A checklist works when the user needs to complete several independent tasks in any order. If the product explains itself, adding UI chrome usually just gets in the way.

Bad onboarding usually comes from a mismatch between the pattern and the job. A tour that explains five features to a user who only needs to send their first invite is not helpful. It's friction with a progress bar. Vercel's write-up on building their virtual product tour makes the same point: start with what the user needs to understand, not with what the product team wants to show.

The activation moment you are actually aiming for

Before you write a single step, name the one event that defines activation for your product. Sending the first invite. Publishing the first project. Connecting the first data source. Creating the first workspace. Pick one. The tour ends there, not at the end of the feature set.

That matters because a tour that keeps going after activation is teaching someone who already succeeded. They do not need more instructions. They need to feel the product working. Stop the tour when the user has done the thing, and let the product take over.

Start the product tour with the smallest useful flow

One user, one job, one path

The first version of an interactive onboarding flow should guide one persona through one path to one outcome. Leave branching out on day one. No "if you're an admin, go here; if you're an end user, go there." Branching doubles the maintenance burden immediately. Every UI change now has to be fixed in two places instead of one.

A short, linear flow also reads like help. A branching flow reads like a decision tree the user did not ask for. Keep it contextual: show the step that matters right now, in the place where it matters.

What to cut from the first draft

First drafts tend to collect a lot of junk: a welcome modal about the company's mission, a "here's what you can do" feature tour, empty copy like "let's get started!", and steps that describe the UI instead of telling the user what to do.

Cut anything that does not move the user toward the activation event you named. Product history is not onboarding. A feature tour is not onboarding. Every step that does not ask the user to do something specific is a step that slows them down. Research on onboarding friction — including PostHog's work on uncovering real user problems — points to the same thing: users drop out when they cannot see the immediate value of the next action.

Build the product tour so each step can find a real element

A minimal React or Next.js anchor pattern

In a React or Next.js app, each tooltip or pointer should anchor to a real DOM element, not a coordinate, not a CSS class that changes during a refactor, and not a selector that matches three things. The pattern that holds up is a stable `data-tour` attribute on the target element:

The tour engine queries `[data-tour="send-invite"]` to position the tooltip. That attribute stays with the component through refactors. It does not break when the button's className changes or when the layout shifts. Use semantic names like `send-invite`, `connect-source`, and `publish-project` so they are easy to read in the DOM and in the tour config.

The selector problem nobody likes debugging

Loose selectors and DOM assumptions are the most common reason tours break silently. A step anchored to `.nav > ul > li:nth-child(3)` breaks the moment someone adds a nav item. A step anchored to a hardcoded pixel offset breaks when the viewport changes. A step anchored to an element's text content breaks when the copy is edited.

The failure mode is annoying: the tour does not error, it just ends up over the wrong element or nowhere at all. The user sees a floating step with no clear target. The fix is to treat the anchor as part of the component contract. If the element matters enough to tour, it gets a stable `data-tour` attribute, and that attribute is treated like an API surface.

Make the product tour survive route changes and SPA navigation

Why the step breaks when the page changes under it

In a single-page app, SPA navigation does not reload the page. It swaps the component tree. If a tour step is waiting to render and the route changes underneath it, the target element may not exist yet, may have unmounted, or may reappear after an async data fetch. The tour does not know any of that. It tries to position against a node that is gone.

This is not a fringe case. Any tour that spans more than one route will run into it. The problem is simple: the tour's step-render cycle and the app's route-render cycle are not synchronized.

The re-entry and resume logic that keeps users moving

State management fixes that. The tour needs to know which step the user is on, which route that step lives on, and whether the target element is mounted before it tries to render. On route change, the tour checks whether this is the route the next step expects and whether the target element is in the DOM. If not, wait. Use a `MutationObserver` or a route-readiness callback, not a `setTimeout`.

For re-entry after refresh or back navigation, store the current step index outside the UI, in `localStorage` or in the user's server-side session. On app load, read the stored step, navigate to the correct route, and resume. Do not restart the tour from step one because the user refreshed. That is a fast way to get them to dismiss it for good.

Write tour copy that sounds like help, not a pop quiz

What each step should say

Each in-app tour step needs three things: what to do, where to do it, and why it matters right now. "Click Send invite to add your first teammate — you'll need at least one to unlock shared projects." That is a complete step. It tells the user what to do, points at the target, and connects the action to a payoff.

It does not need a headline, a product name, an exclamation point, or a sentence that starts with "Welcome to." Keep copy under 25 words per step. If a step needs more than 25 words to explain, it is trying to do too much. Split it or cut it.

How to adapt the flow for roles and segments

Segment by job-to-be-done, not by role label. An admin and a developer may have different activation events, but the real question is: what does this person need to do first? If the answer is the same, they get the same tour. If it is different, they get different tours, not one branching tour.

The practical setup is a single `tourId` parameter passed at initialization. `tourId: "admin-setup"` loads one step config; `tourId: "developer-connect"` loads another. The tour engine stays the same. Only the content changes. That keeps the branching in config, not in component logic.

Store completion, snooze, and re-entry outside the tour itself

The state model your app needs

A product onboarding tour needs five states: not started, in progress, completed, snoozed, and re-entry eligible. Not started means the user has not seen step one. In progress means they've started but not finished. Completed means they hit the activation event. Snoozed means they dismissed the tour and asked to see it later. Re-entry eligible means they completed the tour but can replay it on demand.

Each state exists for a reason. Snoozed without a re-entry path means someone who dismissed the tour on a bad day never gets a second chance. Completed without a replay option means someone who wants to revisit a step has nowhere to go.

Why local hacks turn into maintenance debt

Storing tour state in a cookie, a `window` variable, or a one-off `localStorage` key that only gets written in one place is a maintenance trap. The next time the tour changes, a step is added, the activation event moves, a new segment is introduced, the state model has to be updated everywhere it was written. Miss one place and users see a tour they already finished, or skip one they never started.

Store the state in one place: the user's record on the server, or a single client-side module that owns all reads and writes. The tour queries that module. Nothing else touches it directly.

Test the product tour in staging before users see it

The staging checklist that catches the obvious breaks

Before the tour ships, walk through it in staging with the checklist open:

  • Every step renders on the correct route
  • Every tooltip is positioned against the right element
  • Dismissal works at every step
  • Keyboard navigation moves through steps without a mouse
  • Re-entry from a refreshed page lands on the correct step
  • Snoozed state persists across sessions
  • Completed state blocks the tour from re-triggering

Accessibility is not a later fix. If keyboard focus does not move into the tooltip when it opens, screen reader users get stuck. The WCAG guidance on focus management applies to overlays. The tour is an overlay.

What QA should verify on real screens

Test against the actual UI, not a mock. The happy path in staging is not the same as the happy path in production when a feature flag is off or a data fetch is slow. QA should verify the tour on a fresh account with no existing data, on an account mid-onboarding, and on an account that has already completed the tour. Those are three different states, and each one can expose a different break.

Measure whether the product tour lifts activation, not just clicks

The funnel you need to watch

Track four events: tour started, step N completed for each step, tour completed, and the downstream activation event the tour is supposed to improve. That last event is the one that matters when you want to know whether the tour works. Completion rate tells you whether people finished the tour. Activation lift tells you whether the tour changed behavior.

A tour with 90% completion and no activation lift is a tour people clicked through to make it go away.

How to tell if the tour actually helped

The cleanest signal is a comparison between users who saw the tour and users who did not, measured against the activation event. If you can run an experiment and show the tour to 50% of new users and not the other 50%, the result is easy to read. If you cannot run an experiment, compare activation rates before and after the tour shipped, while controlling for cohort size and traffic source.

The signal that matters is simple: did users who completed the tour reach the activation event at a higher rate than users who did not? If yes, the tour is doing its job. If no, the tour is in the way.

Where Inkly comes in

The maintenance problem this article is really about is not copywriting or anchor patterns. It's that the demo or tour artifact lives somewhere you cannot touch with the tools you already use to ship product. Every time the UI changes, you end up back in a SaaS editor clicking through steps by hand, or re-recording screens, because the artifact is locked in someone else's platform.

Inkly is built on the opposite idea: the demo is code you own, in your repo, authored and maintained by your own coding agent (Cursor, Claude, Codex). When the UI ships a change, you re-prompt against the existing demo code. No re-recording. No manual click-by-click fix. The same three-prompt loop, create, update, produce a variant, works whether you're refreshing a tour after a nav rename or spinning up a version for a new customer segment. The tradeoff is honest: you need a coding agent set up to get the most out of it. If you're already in Cursor or Claude Code, demos as code you own is a natural extension of how you already work.

FAQ

Q: What is the simplest product tour structure that actually helps new users reach activation?

One user, one job, one path to one activation event. The tour starts at the first action the user needs to take and ends the moment they complete the activation event, sending an invite, connecting a data source, or publishing a project. Every step in between asks the user to do something specific. Nothing else belongs in the flow.

Q: How do I decide whether my product needs a tour, a tooltip, a checklist, or nothing at all?

Use a tour when the user needs to complete a sequence of steps across multiple UI elements and cannot figure out the sequence from context. Use a tooltip when there is one unfamiliar element and no sequence. Use a checklist when the tasks are independent and order does not matter. Use nothing when the product is self-explanatory. Adding UI chrome to a clear interface makes it worse, not better.

Q: How do I build a product tour in a custom web app with code?

Add a stable `data-tour` attribute to each target element. The tour engine queries those attributes to position steps, not CSS classes, not coordinates, not text content. Store step state outside the tour UI, server-side or in a single client-side module. Handle route changes by waiting for the target element to mount before rendering the next step. Keep accessibility in the build from the start: move keyboard focus into the tooltip when it opens.

Q: How do I attach tour steps to the right UI elements and handle SPA navigation?

Use `data-tour` attributes as the anchor contract. They travel with the component through refactors and do not break on layout changes. For SPA navigation, synchronize the tour's step-render cycle with the app's route-render cycle. Check that the correct route is active and the target element is in the DOM before rendering. Use a `MutationObserver` or a route-readiness callback instead of a timeout. Store the current step index outside the UI so re-entry after refresh resumes at the right step.

Q: What should each step say so the tour feels helpful instead of annoying?

Three things: what to do, where to do it, and why it matters right now. Under 25 words. No product name, no headline, no exclamation points. If a step needs more than 25 words to explain, it is doing too much. Split it or cut it.

Conclusion

The arithmetic holds at the end as well as the beginning: a tour worth building is short, anchored to real elements, stateful outside the UI, and measured against activation, not completion. If it cannot survive the next deploy, it is not finished yet. Start with one flow, wire one anchor with a `data-tour` attribute, and test it in staging against a fresh account before you add a second step. That's the build. Everything else is maintenance.

Try Inkly

Ship your next demo before the meeting starts

Interactive demos built from your real product and kept current as you ship, done for you.

Book a demo

Keep reading

All posts →