You want to build a Firefox extension. Where do you start, what's different from Chrome, and what do you need to know about Firefox-specific APIs? Here's a practical guide.
WebExtensions: The Shared Standard
Firefox uses the WebExtensions API, which is largely compatible with Chrome extensions. An extension built for Chrome can often be ported to Firefox with minor changes.
But Firefox has important differences:
- Manifest V2 support: Firefox still supports MV2 indefinitely. Chrome is deprecating it.
-
Browser namespace: Firefox uses
browser.*(with Promises); Chrome useschrome.*(with callbacks). Firefox also supportschrome.*for compatibility. -
Container API: Firefox has a
contextualIdentitiesAPI for Multi-Account Containers — unavailable in Chrome. - User script API: Firefox MV3 allows userScripts API (not in Chrome MV3 currently).
- AMO review: Firefox add-ons require code review at addons.mozilla.org — no obfuscation allowed.
Setting Up Your First Extension
Minimum structure:
my-extension/
├── manifest.json
├── background.js (or background/service_worker.js for MV3)
└── icons/
└── icon-48.png
manifest.json (MV2 — still recommended for Firefox):
{
"manifest_version": 2,
"name": "My Extension",
"version": "1.0",
"description": "What it does",
"icons": { "48": "icons/icon-48.png" },
"permissions": ["storage", "tabs"],
"background": {
"scripts": ["background.js"],
"persistent": false
},
"browser_action": {
"default_popup": "popup.html",
"default_icon": { "48": "icons/icon-48.png" }
}
}
For Firefox MV3 (optional, future-proof):
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0",
"background": {
"service_worker": "background.js",
"type": "module"
},
"action": {
"default_popup": "popup.html"
}
}
Note: In MV3, browser_action becomes action, and background pages become service workers.
Loading in Firefox for Development
Without signing (developer mode):
- Open
about:debuggingin Firefox - Click "This Firefox" in left sidebar
- Click "Load Temporary Add-on"
- Select your
manifest.json
The extension loads until Firefox closes. Good for development.
With web-ext (Mozilla's development tool):
npm install -g web-ext
# Run Firefox with your extension loaded (auto-reloads on file changes)
web-ext run --source-dir ./my-extension
# Build a distributable .xpi file
web-ext build
# Sign for distribution (needs AMO API key)
web-ext sign --api-key=$AMO_JWT_ISSUER --api-secret=$AMO_JWT_SECRET
web-ext run is the development workflow — it reloads the extension automatically when files change.
The browser.* vs chrome.* Namespace
Firefox supports both:
// Firefox-native (with Promises):
browser.tabs.query({ active: true, currentWindow: true })
.then(tabs => console.log(tabs[0].url));
// Chrome-compatible (with callbacks):
chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
console.log(tabs[0].url);
});
Best practice for cross-browser extensions: Use the webextension-polyfill library:
npm install webextension-polyfill
import browser from 'webextension-polyfill';
// Now browser.* works with Promises in both Firefox and Chrome
Firefox-Only APIs
contextualIdentities (Multi-Account Containers):
// Create a new container
const container = await browser.contextualIdentities.create({
name: "Work",
color: "blue",
icon: "briefcase"
});
// Open tab in specific container
const tab = await browser.tabs.create({
url: "https://example.com",
cookieStoreId: container.cookieStoreId
});
userScripts API (MV3):
// Register a user script to run on matching pages
await browser.userScripts.register({
js: [{ file: "content-script.js" }],
matches: ["*://*.example.com/*"]
});
These APIs don't exist in Chrome, so they're Firefox-only features.
Storage: Persistent Data
Extensions have access to several storage types:
// Local storage (persists until extension removed or cleared)
await browser.storage.local.set({ key: "value" });
const data = await browser.storage.local.get("key");
// Sync storage (synced via Firefox Sync if user is logged in)
await browser.storage.sync.set({ settings: { theme: "dark" } });
// Session storage (cleared when browser closes) — MV3 only
await browser.storage.session.set({ tempData: "..." });
Capacity limits:
-
local: 5MB by default (can requestunlimitedStoragepermission for more) -
sync: 100KB total, 8KB per item
Content Scripts
Content scripts run in the context of web pages:
// manifest.json
"content_scripts": [{
"matches": ["*://*.example.com/*"],
"js": ["content.js"],
"css": ["styles.css"],
"run_at": "document_end"
}]
// content.js (runs on example.com pages)
document.querySelector('button').textContent = 'Modified by extension';
// Send message to background script
browser.runtime.sendMessage({ type: 'page-loaded', url: location.href });
Important: Content scripts are isolated from the page's JavaScript. They share the DOM but not variables.
Messaging Between Components
// content.js → background.js
browser.runtime.sendMessage({ type: 'getData', id: 123 })
.then(response => console.log(response));
// background.js — receive from content scripts
browser.runtime.onMessage.addListener((message, sender) => {
if (message.type === 'getData') {
return Promise.resolve({ data: fetchSomething(message.id) });
}
});
// popup.js → specific tab
browser.tabs.sendMessage(tabId, { type: 'updateUI' });
AMO Submission: What Firefox Requires
Mandatory for public listing on AMO:
- No obfuscated code. Minification is OK, but deliberately obfuscated code is rejected.
- Source code upload if using build tools (webpack, rollup) — you submit both the built .xpi and a zip of source + build instructions
- Privacy policy required if collecting any data
- Reviewer notes explaining anything non-obvious
Self-distribution: You can sign an extension for self-distribution (unlisted on AMO) if you want to distribute it yourself. Web-ext sign with --channel=unlisted creates a signed .xpi.
Why AMO review matters: AMO-reviewed extensions can't run obfuscated code that could hide malicious behavior. This is why Firefox AMO extensions are generally more trustworthy than Chrome Web Store.
Debugging Extensions
DevTools for extension background script:
- about:debugging → This Firefox → your extension → "Inspect"
- Opens a DevTools window connected to the background script
- Console, breakpoints, network tab all work
DevTools for popup:
- Open the popup (click extension icon)
- Right-click popup content → "Inspect Element"
- DevTools opens for the popup DOM
Content script debugging:
Standard DevTools on the page → Sources tab → content scripts appear under the extension's name.
Publishing
- Create account at addons.mozilla.org (FxA)
- Developer Hub → Submit a New Add-on
- Upload .xpi built by
web-ext build - Complete listing (screenshots, description, category)
- Submit for review
Review times vary: 1-7 days for new submissions, faster for updates. Reviewer feedback is via email.
The review process is rigorous but the reviewers are generally helpful in explaining any issues.
Top comments (0)