Blog

Building a Reliable Element Picker for the Modern Web

Shadow DOM, iframes, dynamic content, z-index stacking: here's what it really takes to build an element picker that works on real-world websites.

Lift14 May, 2026Engineering

An element picker sounds simple. Hover over things, highlight them, click to select. DevTools has one. Browser extensions have them. How hard can it be?

Much harder than you'd think. The web in 2026 is full of edge cases that make naive implementations fall apart completely. We've spent more time on the picker than on any other part of Lift, and we're still finding new ways it can break. It's humbling, honestly.

Why Simple Element Pickers Break on Real Websites

The basic version works by listening to mousemove events on the document, finding the element under the cursor, and drawing a highlight overlay on top of it. This breaks immediately on sites that use pointer-events: none, or sites that intercept mouse events for their own UI: drag-and-drop editors, canvas apps, anything with custom cursors.

We solved this by using a transparent full-screen overlay that captures all mouse events, then using document.elementFromPoint with the overlay temporarily hidden to find the actual element underneath. This sidesteps event interception entirely. Our overlay gets the event, we momentarily hide it, ask the browser "what's at these coordinates now?", and restore it before the next frame.

This approach also prevents the picker from triggering the page's own event handlers. Without the overlay, hovering over a dropdown menu while picking would open the dropdown, moving elements around and making the whole experience unusable. The overlay acts as an event shield. Getting this right felt like solving a puzzle, and it was deeply satisfying.

How to Pierce Shadow DOM for Element Selection

Shadow DOM is a whole different beast. Many component libraries and web components use shadow DOM to encapsulate their markup. A standard document.elementFromPoint call won't pierce shadow boundaries, so you get the host element, not the actual component inside.

Lift recursively walks through shadow roots to find the deepest element under the cursor. When you hover over a component built with shadow DOM, you can select individual elements within it, not just the outer wrapper. This is critical for extracting specific parts of complex web components.

The recursion works like this: we call elementFromPoint to get the top-level element. If that element has a shadowRoot, we call shadowRoot.elementFromPoint at the same coordinates. If that returns a different element with its own shadowRoot, we repeat. We keep going until we reach a leaf node or an element without a shadow root. This handles arbitrarily nested shadow DOM trees, which are surprisingly common in design system implementations.

Handling Iframes and Cross-Origin Security Boundaries

Iframes require yet another approach. Cross-origin iframes are completely inaccessible for security reasons. There's no workaround for this, and we don't try to bypass it. When the user hovers over a cross-origin iframe, we show a clear indicator explaining that the content can't be accessed, rather than silently failing or selecting the iframe element itself. Transparency matters to us.

Same-origin iframes need careful handling to coordinate the picker highlight across frame boundaries. The coordinate systems differ between the parent document and each iframe, so we need to calculate the offset of the iframe within the parent, then transform the cursor position into the iframe's coordinate space. This math gets more complex with nested iframes, scrolled containers, and CSS transforms on the iframe element itself.

Solving the Z-Index Stacking Problem in Chrome Extensions

Our highlight overlay needs to sit on top of everything: above fixed headers, modals, dropdown menus, cookie banners, chat widgets. The web is a chaotic stack of z-indices, and every site manages them differently. It's kind of a mess out there.

We use a combination of a very high z-index (2147483647, the maximum 32-bit integer) and a separate stacking context to stay above page content. We also inject our overlay into the document.documentElement level, outside the body, to avoid being clipped by overflow: hidden containers anywhere in the page hierarchy.

Even this isn't bulletproof. Some sites create stacking contexts with position: fixed and their own maximum z-index values. In rare cases, our overlay can end up behind a modal or fullscreen video player. We detect these situations and dynamically adjust our insertion point when possible.

Drawing Accurate Element Highlights With getBoundingClientRect

Once we know which element the user is hovering over, we need to draw a highlight around it. This seems trivial: get the bounding rectangle, draw a box. But element boundaries are more complex than a single rectangle.

Inline elements can span multiple lines, producing multiple rectangles. Elements with border-radius have rounded corners. Elements with CSS transforms might be rotated, skewed, or scaled. We use getClientRects() for multi-line elements and account for transforms by reading the element's computed transform matrix and applying it to our highlight overlay.

We also show margin, padding, and border dimensions in the highlight overlay, similar to DevTools. This gives users immediate visual feedback about the element's box model, helping them decide whether to select this element or its parent. Small detail, but it makes a real difference in how the picker feels to use.

Keeping the Element Picker Running at 60fps

The picker runs on every mouse move event, which can fire hundreds of times per second. If any part of the pipeline (finding the element, calculating bounds, drawing the highlight) takes too long, the picker feels laggy and unusable. And a laggy picker is worse than no picker at all.

We throttle the element detection to once per animation frame using requestAnimationFrame, and we cache the computed styles and bounding rectangles of recently hovered elements to avoid redundant calculations. The highlight overlay itself is drawn with CSS transforms (translate and scale) rather than top/left/width/height, so the browser can composite it on the GPU without triggering layout recalculations.

Where Our Element Picker Stands Today

The result is a picker that works on most production websites, including complex apps built with React, Vue, Angular, and web components. It handles SPAs that dynamically add and remove elements, sites with complex scroll containers, and pages with dozens of overlapping layers.

It's not perfect. Heavily virtualized lists can cause flickering as elements are recycled, canvas-rendered content is opaque to the DOM and can't be picked, and some sites with aggressive content security policies block our overlay injection. But for the vast majority of real-world websites, it works reliably and feels natural to use. And every time someone tells us it "just worked" on a site they expected to break, it makes our day.