Shadow DOM vs iframe for browser extension tooltips

Shadow DOM vs iframe for browser extension tooltipsJ Now

Opening a new tab to look up a term mid-article is a focus killer. You lose the paragraph, spend 30...

Opening a new tab to look up a term mid-article is a focus killer. You lose the paragraph, spend 30 seconds orienting in the new tab, come back, scroll to find your place. I skip past half the words I should look up for exactly this reason. rabbitholes is a Chrome extension I built to fix this: highlight any text, get an inline explanation from Claude Haiku 4.5 rendered right next to your cursor, and click any word in the response to go deeper — all without leaving the page.

The implementation question that took the most iteration: how do you inject a styled tooltip into arbitrary pages without the host page's CSS destroying it?

Why I didn't use an iframe

Iframes offer hard style isolation. Nothing from the host page gets in. But on font-heavy sites — Medium, Substack, Wikipedia — the tooltip would visibly flash from the browser default font to the correct font as the iframe's stylesheet loaded. I couldn't accept a visible FOUC on every single invocation.

Why shadow DOM works here

A shadow root attached to a host element is isolated from the page's stylesheet without being a separate document. The tooltip renders in one frame — no flash. The host page's CSS can't pierce the shadow boundary without explicit ::part() exposure, and I'm not exposing any parts.

const host = document.createElement('div');
host.style.cssText = 'position:fixed;z-index:2147483647;pointer-events:none';
document.body.appendChild(host);
const shadow = host.attachShadow({ mode: 'closed' });
// Render your React/vanilla tree into shadow here.
// Host page stylesheets stop at the boundary.
Enter fullscreen mode Exit fullscreen mode

mode: 'closed' means element.shadowRoot returns null from outside the extension's script — not an absolute security boundary, but sufficient to prevent CSS and casual JS interference.

The rest of the extension is straightforward Manifest V3: no background server, API keys in chrome.storage.sync, requests go directly from the browser to the API endpoints. Zero telemetry.

https://github.com/robertnowell/rabbitholes