Implementation worksheet · 6 min read
A SPA Tour Target Recovery Specification
Specify what the tour does in four situations, because all four will happen: the target does not exist yet (wait, with a bounded timeout, and say what happens when the timeout expires), the target never appears (skip the step or abandon the tour — decide which, per step), the target exists but is off-screen or covered (scroll into view, then re-measure before drawing), and the target moves after the step is drawn (re-anchor, or accept drift and say so). The default behaviour of most libraries is to fail silently or to anchor to the wrong element, and both look to the user like a broken product rather than a missing step.
Tour libraries are written against a DOM that exists. Single-page applications produce a DOM that arrives late, changes shape on route transitions, and re-renders under the tour's feet. The gap between those two assumptions is where nearly every production tour bug lives, and it is invisible in development because a fast local machine renders before the tour looks.
Put it into practice
1. Make waiting explicit and bounded
Every step needs a maximum wait for its target. Poll or observe, but cap it — an unbounded wait is a tour that appears to hang. Two to five seconds suits most applications; the number matters less than having one, because an unspecified wait is whatever the library happened to implement.
2. Decide skip-versus-abandon per step, not globally
A missing optional step should be skipped quietly. A missing step that the rest of the tour depends on should abandon the tour cleanly rather than continue into nonsense. This is a per-step property and it is the decision that distinguishes a tour that degrades gracefully from one that strands people mid-sequence.
3. Scroll first, measure second
Anchoring to an element's position before scrolling it into view produces a highlight box in the wrong place. Scroll, wait for the scroll to settle, re-measure, then draw. On mobile, also account for the viewport changing when a keyboard or a browser chrome bar appears.
4. Re-anchor on layout change
Observe the target with a resize or mutation observer and re-position the step when it moves. Without this, any lazy-loaded image or expanding panel above the target shifts it out from under the highlight — and the user sees a box pointing at empty space, which reads as a bug in your product.
5. Handle route transitions deliberately
A tour that spans routes needs to know which route each step belongs to. On navigation, either advance to the step matching the new route or end the tour. Continuing a step sequence across an unexpected navigation is how tours end up highlighting an unrelated element on a different page.
6. Log every recovery, with the step and the reason
Waited and found, waited and timed out, skipped, abandoned, re-anchored. Without this you cannot tell a tour nobody finished from a tour that broke for a third of users, and those need completely different responses.
Recovery behaviour, per step
Copy this structure into your review document and record your observed result for each row.
| Situation | Behaviour | Timeout | Logged |
|---|---|---|---|
| Target not yet rendered | wait then proceed | ||
| Wait expired, optional step | skip quietly | n/a | |
| Wait expired, required step | abandon cleanly | n/a | |
| Target off-screen | scroll, settle, re-measure | n/a | |
| Target covered by overlay | raise or skip | n/a | |
| Target moves after draw | re-anchor | n/a | |
| Route changed mid-tour | advance to matching step or end | n/a | |
| Target removed mid-step | close step, log, continue or end | n/a |
A failure worth checking
Silent anchoring to the wrong element. The intended target is missing, the selector matches something else — a second button with the same class, a hidden element with the same test id — and the tour confidently highlights it with the right copy. Nothing errors and nothing is logged. The user reads an instruction pointing at something unrelated, concludes the product is broken, and leaves. Selector specificity plus an explicit not-found path is the fix, and the logging is how you find out it happened.
Common questions
Should tours use test ids or CSS classes as selectors?
Dedicated attributes, and ones that are not shared with tests if your test suite churns. Class-based selectors break on any styling refactor and the break is silent — the tour points somewhere plausible instead of failing. A dedicated attribute makes the dependency visible to whoever next edits the component.
How do I test this without a real slow network?
Throttle deliberately and render the target late on purpose. A test that mounts the target before starting the tour proves nothing, because that is the case that already works. The cases worth testing are the late one and the never one.
Basis and scope
This is a proposed implementation method using illustrative examples, not a measured benchmark or a customer case study. Prepared with AI assistance. Validate product-specific behavior against current documentation and your own test environment.