Cross-Browser
Cross-browser extension compatibility across Chrome, Firefox, and Safari with API normalization and build strategies
You are an expert in cross-browser compatibility for building browser extensions.
## Key Points
- Firefox MV3 uses `"background": { "scripts": [...] }` (persistent event page) instead of `"service_worker"`.
- Firefox requires `browser_specific_settings.gecko.id` for signing and distribution.
- Safari uses Xcode to wrap the extension in a native app container.
- Must be distributed through the Mac App Store or Apple's developer program.
- Some APIs are missing or partially implemented (e.g., `declarativeNetRequest` has limited support).
- Use `browser.runtime.getURL()` consistently; `chrome.*` namespace is not available in Safari.
- Use `webextension-polyfill` to write Promise-based code once and run it across all browsers, rather than maintaining separate callback-based and Promise-based code paths.
- Use feature detection (`typeof chrome.sidePanel !== 'undefined'`) instead of user-agent sniffing to determine available capabilities.
- Maintain a single source tree with a build step that generates browser-specific manifests and includes/excludes platform-specific modules.
- Assuming `chrome.storage` callback-style APIs work identically in Firefox — Firefox's `browser.storage` returns Promises, and mixing paradigms causes subtle bugs; use the polyfill to avoid this.
- Shipping a Chrome extension to Firefox without testing — differences in service worker support, CSP enforcement, and permission prompts can cause silent failures.
## Quick Example
```bash
xcrun safari-web-extension-converter /path/to/extension --project-location /output
```
```bash
npm install webextension-polyfill
```skilldb get browser-extension-skills/Cross-BrowserFull skill: 239 linesCross-Browser Compatibility — Browser Extension Development
You are an expert in cross-browser compatibility for building browser extensions.
Overview
Browser extensions can target Chrome, Firefox, Safari, and other Chromium-based browsers (Edge, Opera, Brave). While the WebExtensions API standard provides a common foundation, each browser has differences in API namespaces, manifest handling, and supported features. A well-structured extension can share the majority of its code across browsers.
Core Concepts
API Namespace Differences
Chrome uses chrome.* APIs, while Firefox supports both browser.* (Promise-based) and chrome.* (callback-based).
// Normalize the API namespace
const api = typeof browser !== 'undefined' ? browser : chrome;
// Firefox's browser.* returns Promises natively
// Chrome's chrome.* historically used callbacks but now also supports Promises in MV3
// Wrapper for environments that may not support Promises
function storageGet(keys) {
return new Promise((resolve, reject) => {
const target = typeof browser !== 'undefined' ? browser : chrome;
const result = target.storage.local.get(keys, (data) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve(data);
}
});
// Firefox returns a Promise directly
if (result instanceof Promise) {
result.then(resolve).catch(reject);
}
});
}
Manifest Differences
Firefox uses some different manifest keys:
// Chrome manifest.json
{
"manifest_version": 3,
"background": {
"service_worker": "background.js"
},
"action": { "default_popup": "popup.html" }
}
// Firefox manifest.json
{
"manifest_version": 3,
"background": {
"scripts": ["background.js"]
},
"action": { "default_popup": "popup.html" },
"browser_specific_settings": {
"gecko": {
"id": "addon@example.com",
"strict_min_version": "109.0"
}
}
}
Key differences:
- Firefox MV3 uses
"background": { "scripts": [...] }(persistent event page) instead of"service_worker". - Firefox requires
browser_specific_settings.gecko.idfor signing and distribution. - Safari uses Xcode to wrap the extension in a native app container.
Safari Considerations
Safari extensions are packaged inside a macOS/iOS app using Xcode. The safari-web-extension-converter tool converts a WebExtension into an Xcode project:
xcrun safari-web-extension-converter /path/to/extension --project-location /output
Safari-specific notes:
- Must be distributed through the Mac App Store or Apple's developer program.
- Some APIs are missing or partially implemented (e.g.,
declarativeNetRequesthas limited support). - Use
browser.runtime.getURL()consistently;chrome.*namespace is not available in Safari.
Implementation Patterns
Build Pipeline for Multiple Browsers
Use a build tool to produce browser-specific bundles:
// build.js
import { readFileSync, writeFileSync, cpSync, mkdirSync } from 'fs';
const browsers = ['chrome', 'firefox', 'safari'];
function buildForBrowser(target) {
const outDir = `dist/${target}`;
mkdirSync(outDir, { recursive: true });
// Copy shared source files
cpSync('src/shared', outDir, { recursive: true });
cpSync(`src/${target}`, outDir, { recursive: true });
// Generate manifest
const base = JSON.parse(readFileSync('src/manifest.base.json', 'utf-8'));
const overrides = JSON.parse(readFileSync(`src/${target}/manifest.overrides.json`, 'utf-8'));
const manifest = { ...base, ...overrides };
if (target === 'firefox') {
// Firefox uses background scripts, not service_worker
manifest.background = { scripts: ['background.js'] };
manifest.browser_specific_settings = {
gecko: { id: 'addon@example.com', strict_min_version: '109.0' }
};
}
writeFileSync(`${outDir}/manifest.json`, JSON.stringify(manifest, null, 2));
}
browsers.forEach(buildForBrowser);
Feature Detection
Prefer feature detection over browser sniffing:
// Check if an API exists before using it
function supportsDeclarativeNetRequest() {
return typeof chrome !== 'undefined' &&
typeof chrome.declarativeNetRequest !== 'undefined';
}
function supportsSidePanel() {
return typeof chrome !== 'undefined' &&
typeof chrome.sidePanel !== 'undefined';
}
// Use the feature if available, fallback otherwise
if (supportsDeclarativeNetRequest()) {
chrome.declarativeNetRequest.updateDynamicRules({ addRules, removeRuleIds });
} else {
// Fallback for browsers without DNR
chrome.webRequest.onBeforeRequest.addListener(
blockHandler,
{ urls: blockList },
['blocking']
);
}
Polyfill Layer
Use the webextension-polyfill library to normalize the API across browsers:
npm install webextension-polyfill
// background.js
import browser from 'webextension-polyfill';
// Now use browser.* everywhere with Promise-based APIs
const data = await browser.storage.local.get('settings');
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
await browser.tabs.sendMessage(tabs[0].id, { type: 'PING' });
Platform-Specific UI Adjustments
function getPlatformInfo() {
if (typeof browser !== 'undefined' && browser.runtime.getBrowserInfo) {
return browser.runtime.getBrowserInfo().then((info) => info.name.toLowerCase());
}
const ua = navigator.userAgent;
if (ua.includes('Firefox')) return Promise.resolve('firefox');
if (ua.includes('Safari') && !ua.includes('Chrome')) return Promise.resolve('safari');
return Promise.resolve('chrome');
}
async function adjustUI() {
const platform = await getPlatformInfo();
if (platform === 'firefox') {
// Firefox popup sizing works differently
document.body.style.width = '400px';
}
if (platform === 'safari') {
// Hide features not supported on Safari
document.getElementById('dnr-section')?.remove();
}
}
Best Practices
- Use
webextension-polyfillto write Promise-based code once and run it across all browsers, rather than maintaining separate callback-based and Promise-based code paths. - Use feature detection (
typeof chrome.sidePanel !== 'undefined') instead of user-agent sniffing to determine available capabilities. - Maintain a single source tree with a build step that generates browser-specific manifests and includes/excludes platform-specific modules.
Core Philosophy
Cross-browser compatibility is not an afterthought to bolt on at the end — it is an architectural decision that shapes how you write every line of extension code. Start with the common WebExtensions API surface and treat browser-specific features as progressive enhancements. The goal is a single codebase with minimal platform-specific branches, not three separate extensions that happen to share some files.
Feature detection should always win over browser sniffing. Checking whether an API exists (typeof chrome.sidePanel !== 'undefined') is robust against new browser versions and Chromium forks you have never heard of. User-agent strings lie, browser detection functions break, but the presence or absence of an API is a ground truth that works everywhere.
Invest in a build pipeline early. Generating browser-specific manifests, including or excluding platform modules, and producing correctly structured ZIPs for each store is tedious work that compounds over time. Automate it once with a simple build script so that every developer and CI run produces consistent, correct outputs for all target browsers.
Anti-Patterns
-
Browser sniffing with user-agent strings — parsing
navigator.userAgentto determine capabilities is fragile, unreliable across Chromium forks, and breaks when browsers change their UA format; use feature detection instead. -
Maintaining separate codebases per browser — forking the entire codebase for Chrome, Firefox, and Safari leads to divergent behavior, duplicated bugs, and triple the maintenance burden; share code and isolate differences behind a thin abstraction layer.
-
Assuming MV3 parity across browsers — Firefox's MV3 implementation uses background scripts instead of service workers, and Safari's MV3 support differs in significant ways; test each browser's actual behavior rather than assuming Chrome's documentation applies everywhere.
-
Hardcoding the
chrome.*namespace — callingchrome.storagedirectly throughout the codebase prevents clean Firefox compatibility; use thewebextension-polyfillor a namespace abstraction to write once and run everywhere. -
Ignoring Safari entirely — Safari's extension ecosystem is smaller but growing, and its WebKit-based webview has unique behaviors around CSP, permissions, and API availability that only surface through testing.
Common Pitfalls
- Assuming
chrome.storagecallback-style APIs work identically in Firefox — Firefox'sbrowser.storagereturns Promises, and mixing paradigms causes subtle bugs; use the polyfill to avoid this. - Shipping a Chrome extension to Firefox without testing — differences in service worker support, CSP enforcement, and permission prompts can cause silent failures.
Install this skill directly: skilldb add browser-extension-skills
Related Skills
Background Workers
Service workers and background scripts for event-driven extension logic, lifecycle management, and persistent tasks
Content Scripts
Content scripts for DOM manipulation, page interaction, and isolated world execution in browser extensions
Extension Publishing
Publishing browser extensions to Chrome Web Store and Firefox Add-ons including review processes and automation
Manifest V3
Chrome Manifest V3 fundamentals including configuration, permissions, and migration from V2
Messaging
Message passing between extension components including content scripts, service workers, popups, and external pages
Popup UI
Building popup and options page UIs for browser extensions with responsive layouts and framework integration