Cross-browser extension development
Cross-browser extension development means writing one extension that ships to Chrome, Edge, and Firefox without forking the codebase. Extension.js compiles a single project into per-browser artifacts (dist/chrome, dist/edge, dist/firefox) and filters the manifest for the active target. The reload loop, runtime APIs, and packaging all match the browser you are debugging.
Use one manifest.json and target specific browsers or custom binaries. Enable polyfills when you need browser.* API compatibility in Chromium-family targets.
Chrome and Firefox extension compatibility
Chrome and Firefox extensions share most of the WebExtensions surface but differ in three places that matter every day:- Background scripts: Chrome (Manifest V3) requires
background.service_worker. Firefox usesbackground.scriptswith non-persistent event pages. Extension.js translates between the two shapes in both directions, so either spelling works from one manifest. - Runtime API namespace: Firefox supports
browser.*natively. Chromium useschrome.*. Pass--polyfillto bridge the gap on Chromium targets. - Permissions and content security policy (CSP): Browsers evaluate some
permissionsandhost_permissionsentries differently. Keep browser-specific values behind prefixed manifest keys instead of feature-detecting at runtime.
Browser-specific manifest fields
Browser-specific manifest fields are how Extension.js keeps a singlemanifest.json working across browsers. At compile time, Extension.js filters prefixed keys (chromium:, chrome:, edge:, firefox:, gecko:) to the active target. Unprefixed keys apply everywhere. See Browser-specific manifest fields for the full prefix list.
Template examples
action

How it works
1) Choose a browser target
Choose where you want to run your extension. Use--browser for common targets, or pass a custom browser binary.
- macOS:
/Applications/Brave Browser.app/Contents/MacOS/Brave Browser - Linux:
/usr/bin/brave-browser(or another Chromium-based browser binary) - Windows:
"C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"
chromium-basedfor--chromium-binarygecko-basedfor--gecko-binary
2) Compile with browser-specific manifest filtering
During compilation, Extension.js includes only the manifest fields for your selected browser. For example:chromium:, chrome:, and edge:.For Firefox-family targets, Extension.js uses
firefox: and gecko:.
See Browser-specific manifest fields
for full details on prefixed manifest fields.
Background translation is bidirectional
You do not need prefixed background keys for the common case. Extension.js translates a plain MV3background in both directions:
- A Gecko build that finds
background.service_workerand nobackground.scriptsrepoints the background at the emitted bundle throughscripts. It dropsservice_workerandtype, which Firefox rejects. - A Chromium MV3 build that finds
background.scriptsfillsservice_workerfrom the first script when no worker is set.
scripts is always removed from the output. Chromium MV3 rejects the whole extension whenever background.scripts is present, even beside a valid service_worker, so the key can never ship in a Chromium build.
3) Output per browser target
Extension.js writes each target to its own build folder:dist/chromedist/edgedist/firefoxdist/chromium-based(custom Chromium engines)dist/gecko-based(custom Gecko engines)
4) Optional browser.* polyfill for Chromium targets
If your code uses browser.*, enable --polyfill for Chromium-family targets:
webextension-polyfill (Mozilla’s browser.* compatibility library) for non-Firefox targets.Extension.js skips this step for Firefox because Firefox already supports
browser.* natively.
The polyfill module resolves from your project first, then from the bundled Extension.js dependency. Package managers may hoist webextension-polyfill to your project root, and a copy you installed yourself wins over the bundled one. When no copy resolves at all, the build prints a warning and continues without the polyfill instead of failing.
Best practices
- Keep one codebase: Put browser differences in prefixed manifest fields when possible.
- Build one target at a time: Generate dedicated artifacts (
dist/<browser>) for each browser in continuous integration (CI). - Use
--polyfillwhen needed: Enable it only when your code depends onbrowser.*in Chromium-family targets. - Check API support early: Use MDN WebExtensions docs before relying on browser-specific APIs.
Next steps
- Learn more about the Browsers available.
- Learn more about Browser-specific manifest fields.

