ScaleBun
Skip to article

Coachmarks & tours

webDeveloper

Anchored guided tours that survive DOM churn — multi-strategy anchor resolution, a fallback ladder that never dead-ends, branching, and per-step analytics.

Updated Reviewed

Coachmarks point at real elements in your product. The hard part is not drawing a tooltip — it is still finding the element three releases later, after the class names changed and the button moved inside a new wrapper.

There is no feature flag. The engine loads lazily the first time you call ScaleBun.coachmarks(), so it costs nothing until you use it.

Start a tour#

TypeScript
const coach = ScaleBun.coachmarks();
// A tour authored in the dashboardcoach.startById('onboarding');
// …or one defined in codecoach.start({  id: 'checkout-intro',  steps: [    {      id: 'cart',      anchor: { strategies: [{ kind: 'data-attr', value: 'cart' }] },      title: 'Your cart',      body: 'Everything you have added lives here.',      advance: 'anchor-click',    },    { id: 'done', title: 'That is it', body: 'Happy shopping.' },  ],});

A step with no anchor renders as a centered modal — useful for an opening or closing card.

Mark your anchors#

The most churn-resistant strategy is an explicit attribute, because it is the only one that is meant to be a hook:

HTML
<button data-scalebun-anchor="checkout">Checkout</button>

Each framework has an idiomatic wrapper for this — ScaleBunAnchor in React, v-scalebun-anchor in Vue, use:scalebunAnchor in Svelte, scalebunAnchor in Angular. See Integrations.

Call coach.reportAnchors() on a page and its anchor keys are reported to the dashboard, so the tour composer offers a list instead of a free-text box.

Anchor resolution#

An AnchorSpec takes a prioritized list of strategies. Resolvers run in order and the first stable hit wins, so a tour survives the change that would have broken a single CSS selector.

StrategyExample
data-attr{ kind: 'data-attr', value: 'checkout' } — most stable
selector{ kind: 'selector', value: '#checkout .cta' }
text{ kind: 'text', value: 'Checkout', scope: 'button' }
aria{ kind: 'aria', role: 'button', name: 'Checkout' }
xpath{ kind: 'xpath', value: '//button[1]' }
nth{ kind: 'nth', selector: '.row', index: 2 }

Open shadow roots are traversed by default (shadowPiercing), and an element must be at least 50% visible to count (minVisibleRatio).

When a later strategy resolves after an earlier one failed, the SDK emits $coachmark_anchor_healed — that event is how you find a selector that has quietly rotted before it fails outright.

Never dead-end#

If an anchor cannot be resolved at all, the step's fallback decides what happens. It defaults to modal, which shows the step centered rather than abandoning the tour.

fallbackBehaviour
modal (default)Show the step as a centered modal, flagged as degraded.
skipMove to the next step.
endEnd the tour.

Failures are reported with a reason — not-found, not-visible, not-clickable, timeout, covered, shadow-closed, iframe-cross-origin — so anchor health is measurable rather than anecdotal.

Advancing#

TypeScript
advance: 'next'            // a Next button or coach.next(). Default.advance: 'anchor-click'    // the user clicks the anchored elementadvance: 'anchor-gone'     // the anchor disappears — they completed the actionadvance: { event: 'saved' } // your app calls coach.notify('saved')advance: { timer: 4000 }   // auto-advance

anchor-click is usually the right choice for a tour that should follow the real product flow rather than talk over it.

Branching#

next takes either a step id or a list of branches; the first whose when passes wins. skipIf drops a step entirely when its predicate passes.

TypeScript
{  id: 'plan',  next: [    { to: 'enterprise-setup', when: { prop: 'plan', op: 'eq', value: 'enterprise' } },    { to: 'self-serve-setup' },  ],}

Predicates are serializable — { prop, op, value }, { flag, is }, { all }, { any }, { not } — which is what lets the dashboard author them too.

Control and observe#

TypeScript
coach.next(); coach.back(); coach.skip(); coach.end();coach.notify('saved');coach.current();                       // { tourId, index, status }
const off = coach.on('$coachmark_step_completed', (e) => console.log(e));

Twelve $coachmark_* events are emitted across the tour lifecycle, and every step is stamped with the current replay coordinate — so you can watch the frame where someone abandoned a step rather than guessing why.

Previewing a draft#

startById accepts a preview token from the dashboard's Preview action, which runs an unpublished tour:

TypeScript
coach.startById('onboarding', { preview: token, resume: false });

Next#

Coachmarks & tours · Web SDK · ScaleBun