Home / Guides

How a visual element picker works on a page that fights back

Pointing at an element sounds simple until the page navigates under you. What a picker has to intercept, what it snaps to, and the selector call that throws.

· By hsynkvlc

Every tag manager tool eventually grows an element picker: turn on a mode, hover the page, click the thing you mean, get a selector back. The idea takes one sentence. Making it work on a real page — one with its own click handlers, its own navigation and its own opinions about z-index — takes rather more.

The click must not reach the page

You are clicking a live page. That click is a real click: the button submits, the link navigates, the modal opens, and the page you were pointing at is gone before the selector is read.

So the picker listens in the capture phase rather than the bubble phase — it sees the event on the way down, before any of the page's own handlers — and then calls both preventDefault() and stopPropagation(). One is not enough: preventDefault stops the browser's default action, stopPropagation stops the page's own listeners from ever seeing it.

The highlight must not eat its own hover

The obvious way to show what you are about to pick is a box drawn over it. The obvious implementation breaks immediately: the moment the box is under the cursor, mouseover fires on the box, not on the element, and the highlight starts following itself.

Two things fix it. pointer-events: none on the overlay, so it is invisible to the mouse. And a guard in the hover handler that ignores the overlay and its badge outright, because a page can still dispatch events at them.

One more detail worth stealing: dimming the rest of the page does not need a second element. A single box-shadow: 0 0 0 9999px rgba(0,0,0,.1) on the highlight paints everything outside it, which is one element instead of four and no layout to keep in sync.

Hover fires far more often than you can draw

mouseover arrives many times per frame across a dense page. Repositioning the box on every one of them is wasted work that shows up as lag on exactly the pages that are hardest to point at. Coalesce to one update per frame with requestAnimationFrame and cancel the pending one each time.

What it should snap to

You click the label; the browser reports the deepest element under the cursor, which is a <span> inside the button. A selector for that span is not what a click trigger wants.

The picker climbs to the nearest ancestor that is actually actionable:

If none of those is above the click, the clicked element itself is the answer. Knowing this list is practical rather than academic: it tells you what the picker will hand you before you press anything.

Tag Master's picker does all of the above and hands back a stable selector, a JS path, the element's attributes and its text, then turns them into a trigger condition and a variable you can copy into GTM.

Escape has to tell someone

A picker that cancels on Escape is table stakes. The part that is easy to miss: the panel that asked for the pick is a different context, and it is waiting. Cancel silently and it waits until whatever timeout it has, showing a spinner over a page where nothing is happening any more. The cancel has to travel back as a result, not as an absence.

The selector call that throws

Checking whether a candidate selector is unique means calling querySelectorAll and counting. That call does not return zero for a malformed selector — it throws. And modern class names are full of characters that make a selector malformed: hover:bg-blue-500 has a colon, md:w-1/2 has a colon and a slash.

So there are two defences, and both are needed. Escape the class names when building the selector, and wrap the match count in a try/catch anyway, because the day it throws unhandled is the day the picker dies silently on a page and you have no idea why.

When hovering is impossible

Some things cannot be hovered into. A dropdown that closes on mouseout, a tooltip that disappears, a menu that collapses the moment the pointer leaves it — the element you want vanishes on the way to clicking it.

The way around it is to stop using the pointer: select the text inside the element the way you would select text to copy, and let the tool read the selection instead. The anchor of a selection is usually a text node, so it is the parent element you actually want. Same selector logic, no hovering involved.

Why any of this matters to you

Because when a picker gives you something surprising, these are the reasons. It snapped to the button rather than the span because of the climb list. It returned nothing because the class names broke the selector. It seemed to hang because a cancel never made it back. Knowing the mechanism turns a tool that sometimes misbehaves into one you can predict.

Related guides

Try it on your own site

Tag Master is free, needs no account, and collects no data.

Add to Chrome — Free