Targeting elements
Spotlight points a tour step, spotlight, tooltip, or hotspot at a DOM element. This page is about how the SDK finds that element at runtime, and how you as the host-app author make it findable without bloating your pages.
Why it matters
Bad targeting is the cause of 90% of tours breaking after a release. An author clicks the "Save" button in the admin picker; the SDK captures div.flex.flex-1 > button:nth-of-type(3); two weeks later somebody adds a "Help" button above Save and the tour silently breaks.
The fix is the same fix teams use for test automation: give the elements you care about a stable, opinionated anchor.
The priority ladder
When the admin picker captures an element, it walks this ladder top-down and stops at the first hit. At runtime, the SDK uses the same ladder to resolve. So the ladder is the contract you're designing against.
data-spotlight="…"— opt-in, wins over everything.- Your team's test-hook attribute —
data-testid,data-qa,data-cy, whatever convention the test suite already uses. The picker's Settings → Selectors tab controls the exact list and order. - A stable HTML
id(see "What counts as stable" below). aria-labelon an interactive element.name=on a form element.role=when unique on the page.title=on the target.- A structural selector — anchored on the nearest ancestor with one of (1)–(7), then tag +
:nth-of-type()below that.
The one rule
Use what your app already has. Don't add
data-spotlightunless nothing else reaches the element.
Spotlight is a drop-in. It shouldn't require you to sprinkle proprietary attributes across your components. Most of the time you already have something stable — a test-hook, a route id, an accessible name — and Spotlight should find it.
Best practice, in order of preference
1. Lean on existing test hooks
If your team uses Cypress (data-cy), Testing Library (data-testid), or Playwright (data-pw), the same attributes double as Spotlight targets. No new attributes, no bloat, and the test suite already guarantees they stay put.
The admin's Settings → Selectors tab lets each tenant pick the order Spotlight checks these in. Default priority is data-spotlight, data-testid, data-test, data-test-id, data-cy, data-qa, data-qa-id, data-automation-id.
<button data-testid="payments-send">Send</button>Picker captures [data-testid="payments-send"]. Refactor the class list, move the button around, doesn't matter — the selector stays.
2. Use semantic IDs on big landmarks
Unique page landmarks — a main form, a primary table, a payment wizard, a checklist — deserve a real id. Spotlight treats these as anchors and builds any structural descendants from them, so even non-anchored children inherit stability.
<main id="request-payment-wizard">
<section>
<h2>Step 1 — pick an account</h2>
<table>…</table>
</section>
</main>A click on the first <td> captures #request-payment-wizard table tr:nth-of-type(1) > td:nth-of-type(1). Anyone rearranging the table heads still leaves the anchor alone, so the selector holds.
The rule: if you can point at a visually-meaningful block and say "this thing has a name", give it an id. Everything below inherits that name as context.
3. Use aria-label on interactive controls
Buttons, links, and form inputs that do one thing usually have an aria-label already (for screen readers). Spotlight uses it.
<button aria-label="Open settings">
<svg>…</svg>
</button>Captures as button[aria-label="Open settings"] — both accessible and stable.
4. Name your form fields
Every <input> / <select> / <textarea> in a form should have name=. Spotlight uses this directly.
<input name="email" type="email" />Captures input[name="email"].
5. Only add data-spotlight as a last resort
If an element has no id, no test hook, no aria-label, and no name, and you still need Spotlight to target it reliably, then add a data-spotlight:
<div data-spotlight="marketing-cta-hero">Reserve it for elements that genuinely lack any other identifier — a decorative marketing block, say. Don't default to it for every tour step; the whole point is to avoid a proprietary attribute on every page.
What counts as a "stable" id
Spotlight's picker rejects ids that look autogenerated:
headlessui-menu-button-42radix-#_r37react-:r5:- Anything with 8+ hex digits at the end (uuid-ish)
- Anything prefixed
:(React 18 automatic ids)
If your framework emits those, either wrap the element with a parent that has a real id, or add one of the test-hook attributes from tier 2.
What counts as a "landmark"
The picker treats these tags as anchor-worthy even without an explicit attribute, provided they're unique on the page:
<main>,<nav>,<header>,<footer>,<aside><section>,<article><form>,<table>,<dialog><ol>,<ul>
So: if your page wraps major regions in semantic HTML (you should be doing this anyway for a11y), Spotlight gets anchors for free.
A concrete example
Before — no anchors, picker ends up with a brittle chain:
<div class="min-h-screen bg-gray-50">
<div class="lg:pl-64">
<main class="flex flex-col">
<div class="flex-1 flex">
<div class="w-full flex-1">
<table class="min-w-full w-full">
<tbody>
<tr class="hover:bg-zinc-100">
<td class="sticky h-16">Select an account</td>Picker captures:
tr:nth-of-type(1) > td:nth-of-type(1)Works today; snaps the moment a second <table> shows up anywhere.
After — one id on the wizard, nothing else changes:
<main id="request-payment-wizard" class="flex flex-col">
<table>
<tr><td>Select an account</td></tr>Picker captures:
#request-payment-wizard table > tbody > tr > td:nth-of-type(1)That's safe against every DOM change outside the wizard — and most changes inside it too. One attribute on one element buys stability for every tour step below.
Diagnosing a broken selector
Admin overlay → open the step editor. The target-summary chip above the selector input turns:
- indigo — one match, we're good
- amber — more than one match; tighten the selector
- rose — zero matches; the element isn't on the current page, or the selector no longer resolves
Amber is the early-warning signal. Don't ignore it.
Performance and bloat
The SDK runtime doesn't add anything to your page. Targeting attributes you've added yourself are free — they're just attributes. What we won't do:
- Generate
data-spotlightattributes at build time. - Run
MutationObserveron the whole document. - Query-select on every frame.
A tour step resolves its target once at start and once more if the element unmounts mid-tour. Static pages cost nothing.