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 }ordiv { box-sizing: content-box } -
button { all: unset }orbutton { width: 100%; background: green !important } span { display: block; color: inherit }- Global
font-size: 62.5%onhtml, which shrinks everyrem-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
-
Both
:host { all: initial }and inlineall: initial !importanton the host element.:hoststops inherited properties (font,color,line-height) at the shadow boundary. Settingall: initial !importantanddisplay: block !importanton the outer<div id="shrinkwatch-badge">stops the page's own#idorbody > divrules from hiding or moving the host. -
Use
px, neverrem. Inside a shadow root,1remstill equals the host page's root<html>font size. If a shop setshtml { font-size: 10px },0.875rembecomes8.75px. Fixedpxvalues look the same on every shop. -
Append to
document.documentElement, notdocument.body. Some single-page shops replace or transformdocument.bodyduring navigation or modal opening (transformonbodybreaksposition: fixeddescendants). Hanging the host off<html>keepsposition: fixedanchored 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.
- Website and install: https://emirmrkaljevic.github.io/shrinkwatch-eu/
- Source (MIT): https://github.com/emirmrkaljevic/shrinkwatch-eu
This article was written with AI assistance. The code, tests and examples come from the repo.
Top comments (0)