Removing Injected CSS with removeCSS

Undo styles injected with chrome.scripting.insertCSS: exact-match rules for removeCSS, keeping CSS in one constant, toggling themes per tab, frames and origins, and what happens after navigation and reloads.

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

A “dark reader” toggle, a focus mode that hides distractions, a highlight style for search results — all inject CSS into the page with chrome.scripting.insertCSS, and all need to take it away again. chrome.scripting.removeCSS exists for exactly that, and it is unforgiving: it removes a stylesheet only if you describe it exactly as it was inserted — same CSS string or files, same target, same origin. Change one character in a template literal between insert and remove and nothing happens, without any error. This guide shows how to make CSS toggles reliable. It belongs to scripting API and dynamic injection.

How inserted styles are identified

insertCSS adds a stylesheet to the target document at the “user” or “author” origin level; it is not a <style> element in the DOM, so page scripts cannot see or remove it and your content script cannot either. The browser keeps a record of each inserted stylesheet keyed by its content — the exact css string or the list of files — plus the target and origin. removeCSS looks for a record with an identical key and removes it. There is no handle or id returned from insertCSS to pass back, so the only way to remove a stylesheet is to reproduce its key precisely. Inserting the same CSS twice creates two records, and one removeCSS removes only one of them.

How removeCSS finds a stylesheetinsertCSS records the stylesheet by its exact CSS text or files, target tab and frames, and origin; removeCSS with the same key removes it, while any difference leaves it in place silently.insertCSScss, target, originBrowser recordkeyed by contentStyles appliednot in the DOMremoveCSS with the same keyExact matchrecord removedAny differenceno-op, no errorInserted twiceremove twice
The CSS text is the identifier — keep it byte-for-byte identical.

Step-by-step: reliable CSS toggles

1. Keep the CSS in one constant or file

1// styles.js
2export const FOCUS_CSS = `
3  aside, [role="complementary"], .sidebar, .ad, .newsletter-signup { display: none !important; }
4  article, main { max-width: 72ch !important; margin-inline: auto !important; }
5`;

Execution context: a shared module imported by the worker. The same string object is used for insertion and removal, so they can never drift. Generating CSS dynamically — for example from a user-chosen font size — is fine as long as you store the exact generated string per tab and reuse it for removal. Using files instead of css avoids the problem entirely, because the file list is the key.

2. Insert and remember what you inserted

 1// sw.js
 2import { FOCUS_CSS } from "./styles.js";
 3
 4export async function enableFocus(tabId) {
 5  const key = `css:${tabId}`;
 6  const { [key]: applied = [] } = await chrome.storage.session.get(key);
 7  if (applied.includes("focus")) return;                       // avoid duplicate inserts
 8  await chrome.scripting.insertCSS({ target: { tabId, allFrames: false }, css: FOCUS_CSS, origin: "USER" });
 9  await chrome.storage.session.set({ [key]: [...applied, "focus"] });
10}

Execution context: the service worker, with scripting permission and host access to the tab. Recording which named styles are active per tab prevents double insertion and survives worker eviction. origin: "USER" makes the stylesheet a user stylesheet, which wins over author !important rules in the cascade — useful for overrides that must stick; "AUTHOR" (the default) behaves like a page stylesheet.

insertCSS options and their effect on removalHow the css or files, target frames and origin options affect what removeCSS must match, and common mismatches.OptionMust match on removalCommon mismatchcssExact stringRe-generated template literalfilesSame listDifferent ordertarget.allFrames / frameIdsSame framesRemoving only from top frameoriginSame valueDefault vs USER
Every option that went into insertCSS must come back out in removeCSS.

3. Remove with the identical description

1export async function disableFocus(tabId) {
2  await chrome.scripting.removeCSS({ target: { tabId, allFrames: false }, css: FOCUS_CSS, origin: "USER" });
3  const key = `css:${tabId}`;
4  const { [key]: applied = [] } = await chrome.storage.session.get(key);
5  await chrome.storage.session.set({ [key]: applied.filter((s) => s !== "focus") });
6}

Execution context: the service worker. The target, CSS and origin mirror the insert exactly. removeCSS resolves successfully even when nothing matched, so your bookkeeping — not the API — is the source of truth for whether a style is active. Firefox and Safari implement removeCSS with the same exact-match semantics.

A focus-mode toggle across a navigationThe user enables focus mode; the worker inserts CSS and records it; the page navigates and the stylesheet disappears; the worker sees the navigation and re-inserts; disabling removes it with the same key.PopupService workerTabfocus oninsertCSS(FOCUS_CSS)navigates → sty…tabs.onUpdated completere-insert (record says on)focus off → removeCSS(FOCUS_CSS)
Navigation clears inserted CSS — re-apply from your record, and remove with the same key.

4. Re-apply after navigation

1chrome.tabs.onUpdated.addListener(async (tabId, change) => {
2  if (change.status !== "loading") return;
3  const key = `css:${tabId}`;
4  const { [key]: applied = [] } = await chrome.storage.session.get(key);
5  if (applied.includes("focus")) {
6    await chrome.scripting.insertCSS({ target: { tabId }, css: FOCUS_CSS, origin: "USER" }).catch(() => {});
7  }
8});
9chrome.tabs.onRemoved.addListener((tabId) => chrome.storage.session.remove(`css:${tabId}`));

Execution context: the service worker. Inserted stylesheets belong to a document; when the tab navigates, the new document starts without them. If the toggle is meant to persist across pages in the tab, re-insert on navigation. Inserting at loading minimises the flash of unstyled content; the .catch covers navigations to pages the extension cannot touch. If the style should apply on every page of a site regardless of tab, a registered content script with a css entry is simpler.

5. Toggle per frame when needed

1await chrome.scripting.insertCSS({ target: { tabId, frameIds: [frameId] }, css: FOCUS_CSS });
2await chrome.scripting.removeCSS({ target: { tabId, frameIds: [frameId] }, css: FOCUS_CSS });

Execution context: the service worker. CSS inserted into a specific frame must be removed from that frame. Inserting with allFrames: true and removing with the top frame only leaves the styles in every iframe. Keep the target object in your record if it varies.

6. Prefer classes for many small variations

1// Insert one stylesheet once, then toggle classes from a content script
2export const THEME_CSS = `
3  html.readable-dark { filter: invert(1) hue-rotate(180deg); }
4  html.readable-dark img, html.readable-dark video { filter: invert(1) hue-rotate(180deg); }
5`;
6// content.js: document.documentElement.classList.toggle("readable-dark", on);

Execution context: the worker inserts the stylesheet once; a content script toggles a namespaced class. When a feature has many states — font sizes, themes, contrast levels — inserting and removing a different stylesheet for each is error-prone. One stylesheet keyed by classes, toggled in the DOM, is easier: removal is a class removal, and the stylesheet itself can stay. Prefix class names to avoid colliding with the page’s own.

7. Clean up when the extension stops

On extension update or disable, inserted stylesheets remain until the page navigates. If that would leave pages broken — a hidden sidebar the user can no longer restore — have the new version’s onInstalled handler remove known stylesheets from open tabs, or reload affected tabs after asking.

Common mistakes

  • Rebuilding the CSS string for removal. Any whitespace difference makes removeCSS a no-op.
  • Mismatched origin. Inserted as USER, removed with the default AUTHOR: nothing happens.
  • Duplicate inserts. Two inserts need two removals; track state to avoid duplicates.
  • Expecting styles to survive navigation. They are per document; re-apply.
  • Trying to remove from a content script. The stylesheet is not in the DOM; only removeCSS can remove it.

Cross-browser variation

  • Chrome / Edge: insertCSS/removeCSS with origin and frame targeting; no-op on mismatch.
  • Firefox: same semantics; cssOrigin naming in the older tabs.insertCSS API, origin in scripting.
  • Safari: supports both calls; test origin: "USER" behaviour, which has differed in some versions.

Verification

  1. Toggle focus mode on and off several times on one page and confirm the page returns exactly to its original appearance.
  2. Navigate within the tab while focus mode is on and confirm it re-applies.
  3. Change one character in the removal CSS in a test build and confirm removal fails — proof that the constant matters.
  4. Close the tab and confirm the css:<tabId> record is removed.

FAQ

Can I list which stylesheets are inserted?

No. Keep your own record.

Does insertCSS work on pages with strict CSP?

Yes. Extension-inserted stylesheets are not subject to the page’s style-src policy.

Is there a limit on inserted stylesheets?

No practical limit, but each insert has a cost; prefer one stylesheet with class toggles.

Should the toggle state follow the user across devices?

The preference can live in chrome.storage.sync (“focus mode on by default”); the per-tab record of what is currently inserted belongs in chrome.storage.session, because it describes documents that exist only in this browser session.

Other Core APIs & Cross-Browser Data Management Resources