Cross-Browser10 min read

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.

C
CWS Kit Team
Share
๐ŸฆŠ

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).

  1. ๐Ÿ”

    Assess Compatibility

    Run the Extension Compatibility Tester to identify API usage that differs between Chrome and Firefox.

  2. ๐Ÿ“„

    Update the Manifest

    Convert manifest.json to handle Firefox-specific fields and remove Chrome-only features.

  3. ๐Ÿ”ง

    Add the Browser Polyfill

    Install webextension-polyfill to use the promise-based browser.* namespace everywhere.

  4. โš™๏ธ

    Fix API Differences

    Handle divergent behavior in storage, messaging, content scripts, and tab management.

  5. ๐Ÿงช

    Test with web-ext

    Use Mozilla's web-ext CLI to lint, run, and debug your extension in Firefox.

  6. ๐Ÿ“

    Handle Content Script Edge Cases

    Fix injection timing, CSS isolation, and host page interaction differences.

  7. ๐Ÿš€

    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.

FeatureAPI / FeatureChromeFirefox
Namespacechrome.* (callback-based)browser.* (promise-based)
Manifest V3Required since Jan 2023Supported, MV2 also still allowed
Service WorkersRequired for background in MV3Supports both service workers and event pages
Side Panel APIchrome.sidePanel (full support)sidebar_action (different API entirely)
Offscreen Documentschrome.offscreen (supported)Not supported โ€” use background page instead
Tab Groupschrome.tabGroups (supported)Not supported
DeclarativeNetRequestFull support with static + dynamic rulesSupported but with some rule limit differences
User Scripts APIchrome.userScripts (MV3)Supported with differences in execution world handling
Storage Sessionchrome.storage.session (supported)browser.storage.session (supported since Firefox 115)
Extension ReviewAutomated (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.

```json { "manifest_version": 3, "name": "My Extension", "version": "1.2.0", "description": "A useful extension", "permissions": ["storage", "activeTab", "sidePanel"], "background": { "service_worker": "background.js", "type": "module" }, "action": { "default_popup": "popup.html", "default_icon": "icon-128.png" }, "side_panel": { "default_path": "sidepanel.html" }, "minimum_chrome_version": "116" } ``` This manifest uses Chrome-specific fields: `sidePanel` permission, `side_panel` key, and `minimum_chrome_version`.

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-dest

The 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.

Key Takeaway

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

View all posts