跳到正文
DEV Community· Emir Mrkaljević·· 3 小時前AI 評分52

ShrinkWatch EU 作者解釋瀏覽器擴充功能介面為何在網站上顯示異常,以及如何用 Shadow DOM 修正

Why your browser extension UI looks broken on half the web (and how a small Shadow DOM wrapper fixes it)

AI 導讀

作者以開源瀏覽器擴充功能 ShrinkWatch EU 為例,説明第三方網站的全域 CSS 會令插入的徽章或覆蓋層顯示異常,並示範以 Shadow DOM 隔離介面。

正文

Emir Mrkaljević

When you inject a small badge or overlay into a third-party website from a browser extension content script, it works fine on your test page. Then you open a real shop and your badge is giant, transparent, stuck in the bottom-left corner, or missing its close button.

I hit this on Monoprix while testing ShrinkWatch EU, an open-source extension that shows unit prices (per kg, per litre or per piece) and shrinkflation warnings on European supermarket web shops.

What the host page does to your DOM

Supermarket frontends are ten years of CSS resets, utility classes and aggressive global selectors piled on top of each other:

  • div { position: relative } or div { box-sizing: content-box }
  • button { all: unset } or button { width: 100%; background: green !important }
  • span { display: block; color: inherit }
  • Global font-size: 62.5% on html, which shrinks every rem-sized rule in your stylesheet

Even if you prefix every class name with sw-, bare element rules (div, span, button) and inherited properties (font, line-height, color, text-align, letter-spacing) still leak into your widget.

The fix: an isolated Shadow Root with :host { all: initial }

Instead of appending your badge directly to document.body and loading a content-script CSS file into the page, create a host element, reset the host itself, and put both the styles and the markup inside a shadow root:

const BADGE_CSS = ':host{all:initial}' +
  '.box{position:fixed;right:16px;bottom:16px;z-index:2147483647;max-width:320px;' +
  'padding:10px 30px 10px 12px;background:#fff;color:#111;border:2px solid #2e7d32;' +
  'border-radius:10px;font:14px/1.4 system-ui,sans-serif;' +
  'box-shadow:0 4px 16px rgba(0,0,0,.2);text-align:left}' +
  '.box.sw-alert{border-color:#c62828;background:#fff5f5}' +
  'div{margin:0}b{font-weight:700;color:#111}span{color:#555}' +
  'button{position:absolute;top:4px;right:6px;border:0;padding:0 4px;' +
  'background:none;font:18px/1 system-ui,sans-serif;cursor:pointer;color:#555}';

function badgeBox() {
  let host = document.getElementById('shrinkwatch-badge');
  if (host && host.shadowRoot) return host.shadowRoot.querySelector('.box');
  if (host) host.remove();
  host = document.createElement('div');
  host.id = 'shrinkwatch-badge';
  host.style.setProperty('all', 'initial', 'important');
  host.style.setProperty('display', 'block', 'important');
  const root = host.attachShadow({ mode: 'open' });
  const style = document.createElement('style');
  style.textContent = BADGE_CSS;
  const box = document.createElement('div');
  box.className = 'box';
  box.setAttribute('role', 'status');
  root.append(style, box);
  document.documentElement.appendChild(host);
  return box;
}

Three details that matter

  1. Both :host { all: initial } and inline all: initial !important on the host element. :host stops inherited properties (font, color, line-height) at the shadow boundary. Setting all: initial !important and display: block !important on the outer <div id="shrinkwatch-badge"> stops the page's own #id or body > div rules from hiding or moving the host.
  2. Use px, never rem. Inside a shadow root, 1rem still equals the host page's root <html> font size. If a shop sets html { font-size: 10px }, 0.875rem becomes 8.75px. Fixed px values look the same on every shop.
  3. Append to document.documentElement, not document.body. Some single-page shops replace or transform document.body during navigation or modal opening (transform on body breaks position: fixed descendants). Hanging the host off <html> keeps position: fixed anchored to the viewport.

Since switching to this wrapper in v0.3.2, the badge has looked the same on every shop checked in a real browser so far.

This article was written with AI assistance. The code, tests and examples come from the repo.

來源:DEV Community · dev.to