Chrome vs Firefox vs Safari API Gap Reference
A reference table of MV3 extension API gaps between Chrome, Firefox and Safari: background model, storage, networking, UI surfaces, identity, scripting and platform features, with the workaround for each gap.
Table of Contents
Before writing a cross-browser feature, you need to know whether the API it depends on exists in each engine, behaves the same, or needs a workaround. Documentation is spread across three vendors, each describing its own engine in its own terms, and compatibility tables list method names without saying what to do about the gaps. This page collects the differences that matter in practice for Manifest V3 extensions, grouped by area, with the workaround for each. Use it at design time to decide what a feature can promise on each browser, and alongside building a capability matrix for your extension to track your own feature set. It belongs to cross-browser API compatibility.
How to read the gaps
Three kinds of difference appear below. Missing APIs do not exist in an engine at all — chrome.offscreen in Firefox, chrome.sidePanel in Safari — and need either a different design or a feature that is simply unavailable there. Divergent APIs exist with different behaviour — blocking webRequest survives in Firefox but not Chrome; alarms fire late in Safari — and need code that tolerates both. Shape differences are the same capability with different names or signatures — sidebar_action versus side_panel, browser.* promises versus chrome.* — and are handled by thin adapters. Versions change: these tables describe the stable channels as of this page’s last update, and the guidance throughout is to feature-detect at runtime rather than trust any table, including this one.
Background and lifecycle
1// Chrome / Safari
2"background": { "service_worker": "sw.js", "type": "module" }
3// Firefox (MV3)
4"background": { "scripts": ["sw.js"], "type": "module" }
Execution context: the manifest per target. Chrome requires a service worker; Firefox’s MV3 background is an event page with a DOM (service worker support is newer and optional); Safari accepts either a service worker or a non-persistent page. Code written for the service worker model runs everywhere.
Workarounds. For DOM work, use an offscreen document in Chrome and the event page’s own DOM in Firefox; in Safari with a service worker, use worker-native APIs or do the work in an extension page. For timing, reconcile scheduled work on startup and when UI opens, as in alarms in Firefox and Safari.
Networking and content blocking
1// Detect blocking webRequest support at runtime (Firefox MV3 keeps it)
2const canBlock = (() => {
3 try {
4 chrome.webRequest.onBeforeRequest.addListener(() => {}, { urls: ["https://example.invalid/*"] }, ["blocking"]);
5 return true;
6 } catch { return false; }
7})();
Execution context: the background, during startup. Registering a throwaway blocking listener on an unreachable URL pattern is a reliable probe: Chrome throws for store-installed extensions, Firefox accepts. Remove the probe listener afterwards in real code.
Workarounds. Express blocking as DNR rules everywhere; add Firefox-only refinements with blocking webRequest behind detection. Check DNR feature support per engine — regex filters, modifyHeaders coverage and rule limits vary — and test static rulesets in each browser before shipping. See declarativeNetRequest rules.
UI surfaces
1// One "open the panel" helper for three engines
2export async function openPanel(windowId) {
3 if (chrome.sidePanel?.open) return chrome.sidePanel.open({ windowId }); // Chrome
4 if (globalThis.browser?.sidebarAction?.open) return browser.sidebarAction.open(); // Firefox
5 return chrome.tabs.create({ url: chrome.runtime.getURL("panel.html") }); // Safari
6}
Execution context: the background or an extension page, called from a user gesture (both sidePanel.open and sidebarAction.open require one). The same HTML page serves all three surfaces.
Workarounds. Build every panel as an ordinary extension page so it can be shown as a side panel, a sidebar, or a tab. Offer popup search where the omnibox is missing. Replace declarativeContent with tabs.onUpdated and action.enable/disable where it is absent. Details in side panel support across browsers and omnibox support and alternatives across browsers.
Data, identity and platform APIs
Workarounds. Use launchWebAuthFlow for sign-in everywhere rather than getAuthToken. Treat bookmarks, history and downloads features as unavailable on Safari and hide them. Native messaging on Safari goes to the containing app, as described in native messaging in Firefox and Safari.
Step-by-step: use the reference in a project
1. List the APIs each feature needs
1export const FEATURE_APIS = {
2 readerMode: ["scripting", "storage"],
3 sidePanel: ["sidePanel"],
4 blocker: ["declarativeNetRequest"],
5 backupExport: ["bookmarks", "downloads"],
6};
Execution context: a shared module. Mapping features to APIs turns this reference into a per-feature verdict.
2. Detect at runtime and record the result
1export function detect() {
2 return {
3 sidePanel: typeof chrome.sidePanel?.open === "function" || typeof globalThis.browser?.sidebarAction?.open === "function",
4 bookmarks: typeof chrome.bookmarks?.getTree === "function",
5 downloads: typeof chrome.downloads?.download === "function",
6 offscreen: typeof chrome.offscreen?.createDocument === "function",
7 };
8}
Execution context: any extension context. Store the result in chrome.storage.session at startup so UI pages can hide unavailable features without probing again.
3. Hide, degrade or replace
For each missing capability, decide: hide the feature (backup export on Safari), degrade it (side panel opens as a tab), or replace it (omnibox becomes popup search). Write the decision into the capability matrix so support staff know what each browser offers.
Common mistakes
- Branching on the user agent. Detect capabilities instead; Edge, Brave and Opera share Chrome’s UA tokens but not always its APIs.
- Assuming Firefox MV3 equals Chrome MV3. The background model and blocking webRequest differ.
- Treating Safari as Chrome with a different store. Many browser-data APIs are absent.
- Trusting old tables. APIs land every few months; re-check at each major release.
- Shipping features that silently do nothing. Hide or explain unavailable features.
Cross-browser variation
This page is the cross-browser variation; for each area, the matrices above list the behaviour per engine. For the mechanics of shipping one package to all three, see shipping one manifest for Chrome and Firefox and porting a Chrome extension to Safari with Xcode.
Verification
- Run
detect()in each browser’s background console and compare with the matrices. - For each feature marked missing on an engine, confirm the UI hides or replaces it there.
- Re-run detection after each major browser release in CI and diff the results.
FAQ
Why not use the WebExtension polyfill to close gaps?
The polyfill converts callbacks to promises and aliases chrome to browser; it cannot add APIs an engine lacks. See using the WebExtension polyfill in MV3.
Does Edge have every Chrome API?
Almost all, but not all — some Google-service APIs, such as identity.getAuthToken, behave differently. Detect rather than assume.
How often do these gaps change?
Every few months. Firefox and Safari have been closing MV3 gaps steadily; re-check before designing around a gap.
Where should I record our own decisions about each gap?
In your capability matrix, next to the feature it affects: the gap, the chosen response (hide, degrade, replace) and the version where you last checked it. That record is what support and product staff need, and it tells you which decisions to revisit when a browser closes a gap.
Related
- Building a capability matrix for your extension — turning this reference into your own table.
- Feature detection instead of browser sniffing — the runtime half.
- Porting a Chrome extension to Firefox — the gaps in practice.
- Cross-browser API compatibility — the parent topic.