How to Port Your Chrome Extension to Firefox
A step-by-step migration guide for porting Chrome extensions to Firefox, covering manifest differences, API polyfills, namespace changes, and AMO submission.
Table of Contents
How to Port Your Chrome Extension to Firefox
Double your audience without rewriting your codebase.
Firefox holds roughly 7% of the global browser market. That sounds small until you do the math. If your Chrome extension has 50,000 users and you capture the same share of Firefox users in your niche, you are looking at another 3,500-4,000 users with minimal extra work. More importantly, Firefox users tend to be technically savvy, privacy-conscious, and loyal โ the kind of users who leave reviews and stick around.
The good news is that Chrome and Firefox both use the WebExtensions API standard. A well-structured Chrome extension can often be ported to Firefox in a single afternoon. The bad news is that "often" is doing heavy lifting in that sentence. There are differences in manifest format, API namespaces, permission handling, and content script behavior that will trip you up if you do not know about them in advance.
This guide covers every step of the migration, from the first manifest change to submitting your extension to addons.mozilla.org (AMO).
- ๐
Assess Compatibility
Run the Extension Compatibility Tester to identify API usage that differs between Chrome and Firefox.
- ๐
Update the Manifest
Convert manifest.json to handle Firefox-specific fields and remove Chrome-only features.
- ๐ง
Add the Browser Polyfill
Install webextension-polyfill to use the promise-based browser.* namespace everywhere.
- โ๏ธ
Fix API Differences
Handle divergent behavior in storage, messaging, content scripts, and tab management.
- ๐งช
Test with web-ext
Use Mozilla's web-ext CLI to lint, run, and debug your extension in Firefox.
- ๐
Handle Content Script Edge Cases
Fix injection timing, CSS isolation, and host page interaction differences.
- ๐
Submit to AMO
Create a Firefox developer account, upload your XPI, and pass the review process.
Step 1: Assess What Needs to Change#
Before touching any code, find out what is compatible and what is not. Mozilla provides an extension compatibility analysis tool, but the fastest approach is searching your codebase for Chrome-specific APIs.
Run this check against your source code:
# Find Chrome-specific API calls that may need changes
grep -rn "chrome\.\(tabGroups\|sidePanel\|offscreen\|declarativeNetRequest\)" src/
grep -rn "chrome\.action\.getUserSettings" src/
grep -rn "chrome\.runtime\.getURL" src/The core WebExtensions APIs โ tabs, storage, runtime, alarms, notifications โ work on both browsers. The problems come from newer Chrome APIs that Firefox either has not implemented or handles differently.
| Feature | API / Feature | Chrome | Firefox |
|---|---|---|---|
| Namespace | chrome.* (callback-based) | browser.* (promise-based) | |
| Manifest V3 | Required since Jan 2023 | Supported, MV2 also still allowed | |
| Service Workers | Required for background in MV3 | Supports both service workers and event pages | |
| Side Panel API | chrome.sidePanel (full support) | sidebar_action (different API entirely) | |
| Offscreen Documents | chrome.offscreen (supported) | Not supported โ use background page instead | |
| Tab Groups | chrome.tabGroups (supported) | Not supported | |
| DeclarativeNetRequest | Full support with static + dynamic rules | Supported but with some rule limit differences | |
| User Scripts API | chrome.userScripts (MV3) | Supported with differences in execution world handling | |
| Storage Session | chrome.storage.session (supported) | browser.storage.session (supported since Firefox 115) | |
| Extension Review | Automated (days to weeks) | Human + automated (usually 1-3 days) |
Step 2: Update the Manifest#
The manifest.json file requires several changes for Firefox. Some are required, some are recommended, and some involve removing Chrome-only fields.
The browser_specific_settings.gecko.id field is critical. Firefox requires a stable extension ID for updates to work correctly. Use an email-format ID like my-extension@yourdomain.com or a UUID format like {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}. Pick one and never change it โ changing the ID means Firefox treats it as a completely different extension.
Step 3: The Browser Polyfill#
Chrome uses the chrome.* namespace with callbacks. Firefox uses the browser.* namespace with promises. You do not want to maintain two sets of API calls. The webextension-polyfill library by Mozilla solves this by providing a browser.* namespace that works on both browsers.
npm install webextension-polyfill// Before: Chrome-specific
chrome.storage.local.get(['settings'], (result) => {
console.log(result.settings);
});
// After: Cross-browser with polyfill
import browser from 'webextension-polyfill';
const result = await browser.storage.local.get('settings');
console.log(result.settings);Include the polyfill in your build. If you use a bundler like webpack, Vite, or Rollup, import it at the top of your entry files. For unbundled extensions, include the browser-polyfill.min.js script in your HTML files before your extension scripts.
One important caveat: the polyfill does not magically make Chrome-only APIs available in Firefox. It normalizes the APIs that both browsers support. If you use chrome.sidePanel, the polyfill will not create a browser.sidePanel in Firefox โ you still need to handle that API difference yourself with feature detection.
// Feature detection for platform-specific APIs
function hasSidePanel(): boolean {
return typeof chrome !== 'undefined' && 'sidePanel' in chrome;
}
function hasSidebarAction(): boolean {
return typeof browser !== 'undefined' && 'sidebarAction' in browser;
}Step 4: Fix Common API Differences#
Even with the polyfill, several APIs behave differently between browsers. These are the ones that cause the most porting bugs.
Step 5: Testing with web-ext#
Mozilla's web-ext CLI tool is indispensable for Firefox extension development. It lints your extension, runs it in Firefox with automatic reloading, and packages it for submission.
# Install globally
npm install -g web-ext
# Lint your extension (catches many compatibility issues)
web-ext lint --source-dir ./dist/firefox
# Run in Firefox with auto-reload
web-ext run --source-dir ./dist/firefox --firefox=firefoxdeveloperedition
# Build the .xpi file for submission
web-ext build --source-dir ./dist/firefox --overwrite-destThe web-ext lint command is particularly valuable during porting. It catches issues like unsupported manifest fields, deprecated APIs, and permission problems before you submit to AMO. Run it as part of your CI pipeline.
For debugging, Firefox's about:debugging#/runtime/this-firefox page lets you inspect your extension's background script, view console logs, and debug content scripts โ similar to Chrome's chrome://extensions developer tools but with Firefox-specific debugging features.
Step 6: Pre-Submission Checklist#
Before uploading to AMO, verify everything works correctly.
Checklist
- manifest.json includes browser_specific_settings.gecko with a stable ID
- web-ext lint passes with no errors
- All chrome.* calls replaced with browser.* (via polyfill or manual)
- Service worker OR background scripts work correctly in Firefox
- Content scripts inject and run on target pages
- Storage read/write works in popup, background, and content scripts
- Extension icons display correctly at all sizes (16, 32, 48, 128)
- Popup opens and closes without console errors
- Options page loads and saves settings correctly
- Sidebar (if applicable) opens via sidebar_action API
- No references to Chrome-only APIs without feature detection fallbacks
- Privacy policy URL is valid and accessible
- Extension description is under 250 characters for AMO listing
Submitting to AMO#
The addons.mozilla.org submission process differs from the Chrome Web Store in important ways. AMO has human reviewers in addition to automated checks, the review is generally faster (1-3 days for initial submission, often within hours for updates), and the listing requirements are slightly different.
Create a developer account at addons.mozilla.org/developers. Upload the .xpi file generated by web-ext build. If your extension uses minified or bundled code, AMO requires you to upload your source code as well โ human reviewers need to be able to read your actual source. Include a README in your source archive explaining how to build the extension from source.
AMO is stricter about certain things: remote code execution is completely forbidden (no loading scripts from external URLs), and user data handling is scrutinized more carefully. But AMO is more lenient about listing content โ your screenshots do not need to meet specific pixel dimensions, and your description can be more technical.
One advantage of AMO: listed extensions receive automatic updates through Firefox's built-in update mechanism, and users can discover your extension through Firefox's add-on recommendations. The discoverability is lower than the Chrome Web Store simply due to market size, but the conversion rate from listing view to install tends to be higher.
Porting a Chrome extension to Firefox is a weekend project, not a rewrite. The core work is three things: update the manifest to handle Firefox-specific fields, add the webextension-polyfill for promise-based API calls, and feature-detect any Chrome-only APIs. Use web-ext lint to catch issues early, test thoroughly with web-ext run, and submit to AMO with your source code included. The 7% market share translates to real users โ often more engaged and loyal than average.
Continue reading
Related articles
Cross-Browser Extension Development in 2026
Build extensions that work everywhere: Chrome, Firefox, Edge, and Safari. Compare WebExtensions APIs, polyfill strategies, and testing workflows for 2026.
XSS Prevention in Chrome Extensions
A security deep-dive into XSS attack vectors specific to Chrome extensions, with practical prevention techniques for popups, content scripts, and background workers.
Lessons From Building 100+ Chrome Extensions
Hard-won lessons about architecture, performance, monetization, reviews, and burnout from a developer who shipped over 100 Chrome extensions across seven years.