Avoiding CSS Conflicts Between Extension and Page

Stop page styles breaking extension UI and extension styles breaking pages: shadow DOM isolation, all: initial resets, prefixed classes, CSS layers, user-origin stylesheets, custom properties and font loading.

Published October 2, 2026 Updated October 2, 2026 8 min read
Table of Contents

Two complaints arrive in the same week. One user says the extension’s panel looks broken on their company intranet — giant serif text, buttons with no padding. Another says that since installing the extension, the “Submit” button on their bank’s site is blue and misaligned. CSS is global by default: the page’s rules apply to everything in its document, including elements your content script adds, and any stylesheet your content script injects applies to the page’s own elements. Preventing conflicts in both directions takes a deliberate isolation strategy. This guide lays out the options from weakest to strongest and when each is appropriate. It belongs to content scripts and DOM injection.

How styles leak in both directions

Inbound leakage — page CSS affecting your UI — happens through three channels. Selectors: the page’s button, div > span, [class*="icon"] rules match your elements. Inheritance: properties like font, color, line-height and letter-spacing flow from the page’s elements into yours. Global resets: * { box-sizing: content-box } or img { max-width: 100% }. Outbound leakage — your CSS affecting the page — happens when stylesheets injected through the manifest’s css key or insertCSS use generic selectors: a .button or .hidden rule in your stylesheet styles every matching element on every site. Each technique below closes some channels and leaves others open.

Isolation techniques and the channels they closePrefixed class names, all: initial resets, CSS cascade layers, closed shadow DOM and extension iframes compared on blocking page selectors, blocking inheritance, and preventing extension CSS from reaching the page.TechniquePage selectorsInheritanceOutbound leaksPrefixed classesNoNoMostlyall: initial on rootPartlyYesNo effect@layer for injected CSSNoNoLower priorityClosed shadow DOMYesWith resetYesExtension iframeYesYesYes
Only shadow DOM and iframes close every channel; the others are partial defences.

Step-by-step: choose the right isolation

1. Default to a shadow root for any injected UI

1// content.js
2import css from "./panel.css?inline";
3const host = document.createElement("readable-panel");
4const root = host.attachShadow({ mode: "closed" });
5root.innerHTML = `<style>:host{all:initial} ${css}</style><div class="panel"></div>`;
6document.documentElement.append(host);

Execution context: the content script in the isolated world. A shadow root blocks page selectors from matching your nodes and keeps your rules from matching the page’s. :host { all: initial } resets inherited properties at the boundary so the page’s font and colour do not flow in. Styles inside the shadow root are scoped automatically, so you can use short class names. This is the right default for panels, buttons, tooltips and dialogs — see injecting UI with shadow DOM without breaking the page.

2. Reset the host element itself

1host.style.cssText = "all: initial; position: fixed; z-index: 2147483646; inset: auto 16px 16px auto;";

Execution context: the content script. The host element lives in the page’s light DOM, so page rules like body > * { display: block; margin: 1em } or readable-panel { display: none } (if a site blocks known extension elements) can target it. Inline styles with all: initial override most of them; for hostile pages, add !important to the properties you rely on.

Where each layer of defence sitsThe host element carries inline resets against page rules, the shadow boundary blocks page selectors, the :host reset stops inheritance, and scoped styles inside the root apply only to extension UI.Page CSSselectors + inheritanceHost inline stylesall: initialShadow boundaryblocks selectorsinside the shadow root:host { all: initial }stops inheritanceScoped stylesshort class names okExtension UIlooks the same everywhere
Defence at the host, the boundary and inside the root.

3. Scope stylesheets that must style the page

1/* injected with insertCSS — styles applied to the PAGE on purpose */
2@layer readable {
3  html.readable-focus aside,
4  html.readable-focus [role="complementary"] { display: none !important; }
5  html.readable-focus .readable-hl { background: #fde68a; color: #111827; }
6}

Execution context: a stylesheet inserted into the page document by the service worker. Some features exist to change the page — hiding clutter, highlighting — and cannot live in a shadow root. Gate every rule behind a namespaced class on <html> that your content script toggles, so the rules apply only while the feature is on; prefix every class you introduce (readable-…); and wrap the rules in a cascade layer. Unlayered page styles beat layered styles of the same importance, which makes your non-!important rules yield to the page’s when you want them to. Use !important only on rules that must win.

4. Choose user origin for overrides that must stick

1await chrome.scripting.insertCSS({ target: { tabId }, css: FOCUS_CSS, origin: "USER" });

Execution context: the service worker. User-origin stylesheets sit in a different cascade origin: for !important declarations, user styles beat author styles regardless of specificity. That makes origin: "USER" the reliable choice for accessibility overrides — minimum font size, forced contrast — that pages should not be able to defeat. Pair it with the exact-match removal rules in removing injected CSS with removeCSS.

Which isolation technique for this CSS?Decision tree: extension-owned UI goes in a closed shadow root; UI that must be fully sandboxed or host untrusted content goes in an extension iframe; rules that intentionally change the page are namespaced and layered, with user origin for overrides.What does the CSS style?our injected UIClosed shadow root:host resetShort class namesscopedcomplex / untrusted UIExtension iframeweb-accessible pageFull isolationpostMessage bridgethe page itselfNamespaced + @layerhtml.readable-*USER originfor must-win overrides
Isolate what you own; namespace what you change.

5. Theme with your own custom properties

1/* inside the shadow root */
2:host { --rd-bg: #ffffff; --rd-fg: #111827; --rd-accent: #2563eb; }
3@media (prefers-color-scheme: dark) { :host { --rd-bg: #1f2937; --rd-fg: #f9fafb; } }
4.panel { background: var(--rd-bg); color: var(--rd-fg); }

Execution context: the shadow root’s stylesheet. CSS custom properties do inherit through the shadow boundary — all: initial does not reset them — so a page that defines --accent or --bg could influence yours if the names collide. Prefix your variables and define them on :host, which wins over inherited values. Following the OS colour scheme keeps your UI readable on dark sites.

6. Load fonts without depending on the page

1@font-face {
2  font-family: "Readable Inter";
3  src: url("chrome-extension://__MSG_@@extension_id__/fonts/inter-var.woff2") format("woff2");
4}
5:host { font: 14px/1.4 "Readable Inter", system-ui, sans-serif; }

Execution context: a stylesheet in the shadow root. @font-face inside a shadow root is not applied in every engine; declaring it in a small stylesheet inserted into the page document (with a uniquely prefixed family name) is the portable approach, and the font file must be web accessible to that page. Simpler still: use system-ui, which needs no file and matches the OS. The __MSG_@@extension_id__ placeholder only works in CSS files loaded as extension resources; in JavaScript-generated CSS, use chrome.runtime.getURL.

7. Use an iframe when isolation must be total

1const frame = document.createElement("iframe");
2frame.src = chrome.runtime.getURL("panel.html");
3frame.style.cssText = "all: initial; position: fixed; inset: auto 16px 16px auto; width: 360px; height: 480px; border: 0; z-index: 2147483646;";
4document.documentElement.append(frame);

Execution context: the content script, embedding an extension page that must be listed in web_accessible_resources. An iframe is a separate document: no styles cross in either direction, and the page cannot read its DOM. The page can still detect, resize or remove the frame. Use this for complex UI built with a framework and its own stylesheet, at the cost of a postMessage bridge for communication and exposure of the frame’s URL — see declaring web accessible resources correctly.

Common mistakes

  • Unprefixed classes in injected stylesheets. .hidden or .btn restyles half the internet.
  • Shadow root without an inheritance reset. The page’s font still flows in.
  • Generic custom property names. --bg collides with page variables through the boundary.
  • !important everywhere. Use it where you must win, not as a default.
  • Forgetting the host element. Page rules can still target it in the light DOM.

Cross-browser variation

  • Chrome / Edge: shadow DOM, @layer, and insertCSS with origin: "USER" all supported.
  • Firefox: same capabilities; adoptedStyleSheets from content scripts is restricted by Xray wrappers, so use <style> elements in shadow roots.
  • Safari: shadow DOM and @layer supported in current versions; @font-face inside shadow roots has historically been ignored — declare fonts in the page document.

Verification

  1. Inject a stylesheet like * { font: 30px serif !important; color: red !important; } into a test page and confirm your UI is unaffected.
  2. Load the extension on a page with classes named like yours and confirm the page’s elements are not restyled.
  3. Toggle the page into dark mode and confirm your UI follows the OS scheme, not the page’s variables.
  4. Remove the extension’s page-level stylesheet and confirm the page returns exactly to its original look.

FAQ

Is shadow DOM enough against hostile pages?

It blocks styling. It does not stop a page from removing or hiding your host element. Watch for removal if the UI matters.

Can I use Tailwind in a shadow root?

Yes — build the CSS and insert it into the shadow root as a string. Set Tailwind’s preflight to scope to :host, or disable it and rely on your own reset.

Do content scripts’ CSS files from the manifest go into the shadow root?

No. Manifest css files are injected into the page document. Use them only for page-level styling.

Other MV3 Architecture & Extension Lifecycle Resources