Firefox Add-on Development
Firefox Add-ons are based on WebExtensions API — the same specification as Chrome Extensions. Most Chrome extensions can be ported to Firefox with minimal changes. However, there are differences in details: API support, signing procedure, extension engine.
Firefox vs Chrome differences
| Aspect | Firefox | Chrome |
|---|---|---|
| API namespace | browser.* (Promise) + chrome.* |
chrome.* (Callback) |
| Manifest | MV2 and MV3 (MV3 added in Firefox 109+) | MV3 only (MV2 deprecated) |
| Background | Persistent background page (MV2) or SW (MV3) | Service Worker only |
| Signing | Required via AMO | Not required |
browser_style |
Supported | Not supported |
Manifest V2 (Firefox — most stable variant)
{
"manifest_version": 2,
"name": "My Firefox Add-on",
"version": "1.0.0",
"description": "Add-on description",
"permissions": [
"storage",
"tabs",
"activeTab",
"https://*.example.com/*"
],
"background": {
"scripts": ["background.js"],
"persistent": false
},
"browser_action": {
"default_popup": "popup.html",
"default_icon": {
"48": "icons/icon48.png",
"96": "icons/icon96.png"
}
},
"content_scripts": [{
"matches": ["https://*.target-site.com/*"],
"js": ["content.js"]
}],
"options_ui": {
"page": "options.html",
"open_in_tab": false
}
}
Promise-based API
Firefox supports native Promises in browser.*. The webextension-polyfill polyfill makes code compatible with Chrome:
import browser from 'webextension-polyfill';
// Firefox — native promises
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
const tab = tabs[0];
// Execute script (MV2)
await browser.tabs.executeScript(tab.id, {
code: 'document.body.style.background = "yellow"',
});
// Storage
await browser.storage.local.set({ key: 'value' });
const result = await browser.storage.local.get('key');
Content Script: limitations and workarounds
In Firefox, Content Scripts run in an isolated world (Xray wrapper) — they don't have direct access to JavaScript objects on the page. For interaction with the page, you need window.postMessage or CustomEvent:
// Content script -> Page script
window.postMessage({ source: 'myExtension', type: 'REQUEST_DATA' }, '*');
// Page script handler
window.addEventListener('message', (e) => {
if (e.data?.source === 'myExtension' && e.data?.type === 'REQUEST_DATA') {
window.postMessage({
source: 'myPage',
type: 'RESPONSE_DATA',
payload: window.__MY_APP_STATE__,
}, '*');
}
});
Signing and publishing
Firefox requires signing from Mozilla for any extension not distributed through AMO:
# web-ext — official CLI from Mozilla
npm install -g web-ext
# Development with hot reload
web-ext run --source-dir ./dist --firefox-binary "/path/to/firefox"
# Build
web-ext build --source-dir ./dist --artifacts-dir ./artifacts
# Signing (requires AMO API keys)
web-ext sign \
--source-dir ./dist \
--api-key $AMO_JWT_ISSUER \
--api-secret $AMO_JWT_SECRET
To get API keys: addons.mozilla.org/developers/addon/api/key/.
Temporary installation for testing
Without signing, the add-on can only be installed via about:debugging:
- Open
about:debugging#/runtime/this-firefox - "Load Temporary Add-on"
- Select
manifest.json - Works until browser restart
For corporate deployment without AMO — use policies.json in Firefox Enterprise.
Firefox specifics: contextualIdentities (containers)
Firefox supports Containers — isolated contexts with separate cookies. Access via API:
const containers = await browser.contextualIdentities.query({});
// { cookieStoreId, name, color, icon }
// Open tab in specific container
await browser.tabs.create({
url: 'https://example.com',
cookieStoreId: 'firefox-container-1',
});
Timeline
Firefox Add-on adapted from Chrome extension, with signing and AMO publication — 2–3 additional days on top of ready Chrome extension. New add-on development from scratch — 4–8 days depending on functionality.







