Building an on-page tooltip that doesn't break the host page turns out to have one real decision point: shadow DOM or iframe overlay.
I went with shadow DOM for rabbitholes — a Chrome extension that renders inline explanations next to your cursor when you highlight text. The extension lets you click any word in the explanation to go deeper, chaining lookups without ever leaving the page.
The iframe path seems obvious at first. Iframes sandbox your content, so style collisions are impossible. But iframes can't read the host page's computed font stack. You either ship a fallback font that visibly differs from the article you're reading, or you FOUC on every load while you inspect and mirror the host's fonts via JavaScript. Neither is acceptable when the whole point is a tooltip that feels native to the page.
Shadow DOM gives you a separate style scope without the isolation penalty. CSS custom properties still pierce the shadow boundary if you explicitly forward them, but regular host-page styles don't leak in. That means I can write tooltip CSS that's fully predictable — no specificity wars with the host stylesheet — while still inheriting font-family from the root if I choose to.
The tradeoff is real: shadow DOM doesn't fully isolate JavaScript. A page running aggressive global event listeners can interfere. In practice this matters less for a read-only tooltip than it would for an interactive form, but it's worth knowing before you commit.
The implementation here:
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'closed' });
const style = document.createElement('style');
style.textContent = `/* tooltip styles, isolated */`;
shadow.appendChild(style);
// append tooltip markup to shadow, not document.body
Closed mode (mode: 'closed') means the shadow root isn't accessible via element.shadowRoot from page scripts — one less surface for a hostile page to poke at.
The extension itself (rabbitholes) sends highlighted text directly to Claude Haiku and optionally enriches answers with Brave Search results. No intermediary server — requests go from the browser straight to api.anthropic.com and api.search.brave.com. Manifest V3, API key stored in chrome.storage.sync.
Top comments (0)