Why Firefox Rebuilt Its Extension System — and What Migrating Actually Looks Like
Firefox's move from legacy XUL add-ons to the sandboxed WebExtensions API traded low-level power for security and cross-browser portability. Here's the reasoning and a minimal migration example.
14 Aug 2026, 00:37 UTC

If you maintained a Firefox add-on before 2017, you remember the pain: XUL overlays, the Add-on SDK, and APIs that broke with every major browser release. Firefox's decision to drop that legacy stack in favor of the WebExtensions API was controversial, but it solved real problems. This post explains the engineering reasoning behind the switch and walks through what a minimal migration actually involves.
The problem with the legacy extension model
Legacy Firefox extensions were essentially privileged JavaScript with near-total access to the browser's internals. They could hook into XUL (XML User Interface Language, Firefox's old UI markup), monkey-patch browser functions, and touch almost anything. That power came with three costs:
- Fragility. Because extensions depended on internal implementation details, any refactoring of Firefox's UI or plumbing could silently break them. Mozilla's own engineers had to tiptoe around popular add-ons.
- Security exposure. A malicious or compromised add-on had effectively the same privileges as the browser itself — full file system, network, and credential access.
- Fragmentation. Chrome had its own extension API, Firefox had another, Safari a third. Cross-browser extension authors maintained parallel codebases.
The thesis behind the change: extensions should run in a sandboxed, permission-declared model with a stable, cross-browser API surface — even if that means giving up some low-level power.
What WebExtensions actually changes
WebExtensions replaces internal hooks with a documented API namespace, browser.* (with a callback-style chrome.* alias for compatibility). Extensions declare what they need in a manifest.json file, and the browser enforces those permissions at runtime. UI is built with ordinary HTML/CSS instead of XUL. Background logic runs in a dedicated background page or, in later manifest versions, a service worker.
Firefox's implementation matured around version 78 (the ESR line), which supports the bulk of the standard API surface. A small set of privileged or experimental APIs remains Firefox-specific, so assume Firefox ≥78 and verify any less-common API against the current MDN compatibility tables before relying on it.
Worked example: a toolbar button, old and new
Consider the classic hello-world add-on: a toolbar button that shows a message. The legacy Add-on SDK version lived in index.js and depended on SDK modules:
// Legacy Add-on SDK (no longer supported)\nvar buttons = require('sdk/ui/button/action');\nbuttons.ActionButton({\n id: \"hello\",\n label: \"Say hello\",\n icon: \"./icon.png\",\n onClick: function() { require('sdk/tabs').activeTab.url; }\n});The WebExtensions equivalent is three small files. First, the manifest:
{\n \"manifest_version\": 2,\n \"name\": \"Hello Button\",\n \"version\": \"1.0\",\n \"browser_action\": {\n \"default_icon\": \"icon.png\",\n \"default_title\": \"Say hello\"\n },\n \"background\": { \"scripts\": [\"background.js\"] },\n \"permissions\": [\"notifications\"]\n}Then background.js, using the standard API:
browser.browserAction.onClicked.addListener(() => {\n browser.notifications.create({\n type: \"basic\",\n iconUrl: \"icon.png\",\n title: \"Hello Button\",\n message: \"Hello from WebExtensions!\"\n });\n});To test it locally: open about:debugging#/runtime/this-firefox in Firefox 78+, click Load Temporary Add-on, and select the manifest.json. No special permissions are needed beyond a normal Firefox profile. Click the new toolbar icon — a notification should appear. Open the Browser Console (Ctrl+Shift+J) and add a console.log in the listener to confirm the browser.* APIs resolve; if an API is missing in your version, the console will show it as undefined, which is your signal to check MDN's compatibility table.
The trade-off: security at the cost of depth
The migration was not free. Legacy extensions could do things WebExtensions deliberately forbids: restyling the browser chrome itself, intercepting arbitrary internal events, or shipping native code. Popular power-user add-ons died in the transition because their entire feature set depended on XUL manipulation. That is the honest cost of the sandbox model: you gain a stable API, per-extension permission prompts, and cross-browser portability, but you lose the ability to reach into Firefox's guts. If your add-on idea fundamentally requires modifying browser UI beyond what browser_action/sidebar_action expose, WebExtensions may simply not support it — check before you build.
One more caveat: behavior of some APIs (storage quotas, notification rendering, service-worker lifecycles under Manifest V3) differs across Firefox releases and across browsers. Treat MDN as the source of truth and test on the oldest Firefox version you intend to support.
What to do next
- Read the MDN WebExtensions documentation — it is the authoritative reference and includes per-API browser compatibility tables.
- Prototype with a temporary add-on via
about:debugging, then graduate toweb-ext(Mozilla's CLI) for linting, running, and packaging. - Verify your extension on Firefox 78+ (or your chosen minimum) and use the Browser Console to confirm every API call resolves.
- When ready, submit to addons.mozilla.org (AMO); signing through AMO is required for permanent installation in release Firefox.
The WebExtensions transition traded raw power for maintainability, security, and portability. For the vast majority of extensions, that trade was clearly worth it — and the migration path for simple add-ons is genuinely short.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.