Per-Site Settings and Overrides
Let users configure an MV3 extension differently on each site: a global-plus-overrides settings model, hostname and registrable-domain keys, storage layout within sync quotas, a popup toggle, and applying overrides.
Table of Contents
Users want the extension on everywhere except their bank, a larger font on one news site, dark mode on most pages but not on the design tool they use for work. A single set of global options cannot express that, and a per-site list for every option quickly becomes unmanageable. The model that scales is global defaults plus sparse overrides: every site uses the global settings unless the user has overridden a specific setting for that site. This guide builds the data model, storage layout, resolution logic and the popup control that makes overrides one click away. It belongs to options page configuration.
The model: defaults, globals, overrides
Resolution happens in three layers. Defaults ship in the code. Global settings are what the user changed in the options page. Site overrides are the few settings the user changed for a particular site. The effective settings for a page are defaults, then globals, then that site’s overrides, each layer replacing only the keys it contains. Overrides stay sparse — a site entry contains only what differs — which keeps storage small and makes “reset this site” a matter of deleting one entry. The other key decision is what “site” means: an exact hostname (docs.example.com), or a registrable domain that covers all its subdomains (example.com). Users usually think in registrable domains; a few features need hostname precision.
Step-by-step: overrides that scale
1. Choose the site key
1// shared/site-key.js — registrable domain with a small public-suffix table (use a PSL library in production)
2const MULTI_PART_SUFFIXES = new Set(["co.uk", "com.au", "co.jp", "com.br", "github.io"]);
3
4export function siteKey(url) {
5 const host = new URL(url).hostname.replace(/^www\./, "");
6 const parts = host.split(".");
7 const lastTwo = parts.slice(-2).join(".");
8 return MULTI_PART_SUFFIXES.has(lastTwo) ? parts.slice(-3).join(".") : lastTwo;
9}
Execution context: a shared module. Keying by registrable domain means one override covers www.example.com, docs.example.com and example.com, which matches how users think. The naive “last two labels” rule breaks for suffixes like co.uk and for shared hosting domains like github.io, where each subdomain belongs to a different person — a public suffix list library handles them correctly. Use exact hostnames instead if the feature genuinely differs per subdomain.
2. Store globals and overrides separately
1// storage layout (chrome.storage.sync)
2// "settings": { theme: "dark", enabled: true }
3// "site:example.com": { fontScale: 1.3 }
4// "site:bank.example": { enabled: false }
5export async function setSiteOverride(key, patch) {
6 const k = `site:${key}`;
7 const { [k]: current = {} } = await chrome.storage.sync.get(k);
8 const next = { ...current, ...patch };
9 for (const [name, value] of Object.entries(next)) if (value === undefined) delete next[name];
10 await (Object.keys(next).length ? chrome.storage.sync.set({ [k]: next }) : chrome.storage.sync.remove(k));
11}
Execution context: the service worker or an extension page. One key per site keeps each item well under chrome.storage.sync’s 8 KB per-item limit and lets two devices edit overrides for different sites without overwriting each other. Removing a site’s key when its overrides become empty keeps storage tidy. With hundreds of sites, watch the 512-item and 100 KB totals — move rarely used site entries to local or cap the list.
3. Resolve effective settings for a page
1import { DEFAULTS } from "./settings.js";
2
3export async function effectiveSettings(url) {
4 const key = siteKey(url);
5 const { settings = {}, [`site:${key}`]: override = {} } =
6 await chrome.storage.sync.get(["settings", `site:${key}`]);
7 return { ...DEFAULTS, ...settings, ...override, _site: key, _overridden: Object.keys(override) };
8}
Execution context: a content script or the worker. Two keys are read per page — globals and the site entry — which is fast. Returning which keys are overridden lets the UI show “custom for this site” markers next to those settings and offer a reset.
4. Let content scripts react to their own site only
1// content.js
2const key = siteKey(location.href);
3let current = await effectiveSettings(location.href);
4apply(current);
5
6chrome.storage.onChanged.addListener(async (changes, area) => {
7 if (area !== "sync") return;
8 if (!("settings" in changes) && !(`site:${key}` in changes)) return; // another site's override
9 current = await effectiveSettings(location.href);
10 apply(current);
11});
Execution context: a content script. Filtering change events to the global key and this page’s own site key means editing an override for one site does not make every open tab re-resolve. See reacting to option changes in every context.
5. Put the override control where the user is
1// popup.js
2const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
3const key = siteKey(tab.url);
4const eff = await effectiveSettings(tab.url);
5scopeLabel.textContent = `Settings for ${key}`;
6enabledToggle.checked = eff.enabled;
7enabledToggle.addEventListener("change", () => setSiteOverride(key, { enabled: enabledToggle.checked }));
8resetBtn.hidden = eff._overridden.length === 0;
9resetBtn.addEventListener("click", () => chrome.storage.sync.remove(`site:${key}`));
Execution context: the popup, which knows the current tab through activeTab. “Disable on this site” and “Text size on this site” belong in the popup, where the user notices the problem. The options page then lists all overridden sites with their differences and lets the user edit or remove them in bulk.
6. Show overrides in the options page
List each site entry with a summary of what differs (“Disabled”, “Text 130%”), a remove button, and a search box for long lists. Sorting by most recently changed puts the sites the user is thinking about at the top. Store a changedAt timestamp inside each override to support it.
7. Respect overrides in the worker too
Settings applied by the service worker — dynamic content script registrations, network rules, icon state per tab — must use the same resolution. For example, a disabled site should be added to the content script’s excludeMatches, so the script does not even load there, rather than loading and exiting.
Common mistakes
- Keying by hostname when users mean the site.
wwwand the apex get different settings. - Naive registrable-domain logic.
co.ukandgithub.iobreak it. - All overrides in one sync item. Hits the 8 KB limit and causes cross-device conflicts.
- Storing full copies of settings per site. Global changes then never reach those sites.
- Overrides only in content scripts. The worker applies some settings too.
Cross-browser variation
- Chrome / Edge:
storage.syncquotas as described; popup knows the tab throughactiveTab. - Firefox: same model;
storage.syncuses Firefox Sync. Container tabs share the same site settings unless you key bycookieStoreIdtoo. - Safari: per-site permissions are managed by Safari itself; your per-site settings model works alongside it.
Verification
- Set a global theme, then override font size for one site; confirm that site uses both and others use only the global.
- Change the global theme and confirm the overridden site picks it up (overrides are sparse).
- Reset the site and confirm its key is removed from storage.
- Visit
docs.example.comandwww.example.comand confirm they share theexample.comoverride.
FAQ
Should overrides sync across devices?
Usually yes — they express preferences about sites, which are the same everywhere. Device-specific overrides belong in local.
How do I handle sites with many subdomains owned by different people?
Treat them as separate sites; that is exactly what the public suffix list encodes.
Can managed policy force settings on certain sites?
Yes — read chrome.storage.managed as an extra layer above overrides, as in configuring extensions with enterprise managed storage.
Can users export their site overrides?
Include them in the settings export described in exporting and importing extension settings — they are usually the most personal part of a user’s configuration.
Related
- Defaulting and versioning an options schema — the defaults layer.
- Syncing options across a user’s devices — sync quotas and conflicts.
- Search and filter in a large options page — listing many site overrides.
- Options page configuration — the parent topic.