Building Your First Firefox Extension: A Getting-Started Guide

Firefox extensions use the WebExtensions API, a cross-browser extension standard that shares large portions of its API surface with Chrome’s extension API. An extension written for Chrome will, with minor adjustments, work in Firefox. An extension written to the WebExtensions standard works in both, with the occasional vendor-specific difference to handle. This guide covers the mechanics of getting a Firefox extension from zero to working in the browser, then to the Firefox Add-ons store.

Extension Anatomy

Every extension starts with a manifest — manifest.json in the root of the extension directory:

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0",
  "description": "A brief, honest description",
  "permissions": ["storage", "activeTab"],
  "action": {
    "default_popup": "popup.html",
    "default_icon": "icons/icon-48.png"
  },
  "background": {
    "scripts": ["background.js"]
  },
  "content_scripts": [
    {
      "matches": ["https://*.example.com/*"],
      "js": ["content.js"]
    }
  ]
}

The manifest declares what the extension does and what APIs it needs. The MDN manifest.json reference is the authoritative resource — each key has its own page with browser compatibility notes.

The Three Main Extension Components

Background script (or background page). Runs persistently (background page, MV2) or event-driven (service worker, MV3). Handles browser events: tab creation, network requests, alarms, messages from other extension contexts. Has access to all declared extension APIs.

Content scripts. Injected into web pages based on URL patterns declared in the manifest. Can read and modify the page DOM. Limited extension API access — mainly chrome.runtime.sendMessage and chrome.storage. Runs in an isolated world (separate JavaScript scope from the page).

Popup and options pages. Standard HTML pages with extension API access. The popup appears when the user clicks the extension icon. The options page is for settings. Both have full access to declared extension APIs.

Loading an Unpacked Extension in Firefox

For development, load the extension without packaging:

  1. Open about:debugging#/runtime/this-firefox
  2. Click “Load Temporary Add-on…”
  3. Navigate to your extension’s manifest.json and select it

The extension loads and appears in the toolbar. It’s temporary — it’s removed when Firefox restarts. Use this for development iteration.

For a persistent development installation that survives browser restarts, use web-ext from Mozilla:

npm install --global web-ext
web-ext run

web-ext run watches for file changes and reloads the extension automatically — much faster development cycle than manual reloading.

Firefox vs Chrome: The Key Differences

The WebExtensions API is designed for cross-browser compatibility, but differences exist:

Namespace. Firefox uses browser.* APIs with Promise-based returns. Chrome uses chrome.* with callbacks (though Chrome has added Promise support for most APIs in recent versions). Firefox also recognizes the chrome.* namespace for compatibility, so Chrome-written extension code using chrome.* runs in Firefox.

For new extensions targeting both browsers: either write with browser.* and use Mozilla’s webextension-polyfill for Chrome compatibility, or use chrome.* and test that it works in Firefox (it mostly does).

Manifest V3 differences. Firefox’s MV3 implementation differs significantly from Chrome’s in one important area: webRequest blocking. Chrome removed webRequestBlocking in MV3, requiring content blockers to use the declarativeNetRequest API with pre-compiled rulesets. Firefox maintains webRequestBlocking in its MV3 implementation, which is why uBlock Origin’s full feature set still works in Firefox MV3 but is limited in Chrome MV3.

Background page vs service worker. Firefox supports both background pages (MV2) and background service workers (MV3). The Firefox-specific extension ID uses a {uuid} format that must be declared to get a stable extension ID across development reloads.

Content Security Policy. Firefox has slightly different defaults for extension page CSP. If your extension pages work in Chrome but fail CSP checks in Firefox, check the policy declaration.

Your First Working Extension: A Tab Counter

A minimal working example that shows the tab count in the extension icon badge:

// background.js
async function updateBadge() {
  const tabs = await browser.tabs.query({});
  browser.action.setBadgeText({ text: String(tabs.length) });
  browser.action.setBadgeBackgroundColor({ color: '#4a90d9' });
}

browser.tabs.onCreated.addListener(updateBadge);
browser.tabs.onRemoved.addListener(updateBadge);
updateBadge();

Add "tabs" to permissions in manifest.json. The action manifest key creates the browser toolbar button; setBadgeText puts text on the icon.

This demonstrates the event-driven pattern: register listeners for browser events, update state when they fire.

Debugging

Open the extension DevTools from about:debugging:

  • Click “Inspect” next to your extension
  • This opens a DevTools window for the background script/service worker
  • Console output from the background appears here

For content scripts: open the DevTools for the page the content script is running in (F12). Content script logs appear in the page’s console, labeled with the extension source.

Permissions: What to Declare

Firefox’s permission model is the same as Chrome’s. Request only what you need — the review process scrutinizes broad permissions, and users see permission requests at install time.

Common permissions:

  • activeTab — access to the current tab when the user clicks the extension icon. Does not require approval for access; grants it on user action.
  • storage — access to browser.storage.local and browser.storage.sync
  • tabs — access to the full tabs API (URLs, titles, navigation events)
  • <all_urls> — content script access to all sites. Triggers a warning at install and review scrutiny.

Publishing to Firefox Add-ons

Unlike Chrome’s $5 fee, creating a Firefox developer account is free at addons.mozilla.org.

Firefox requires code signing for extensions distributed outside of about:debugging development mode. The Add-ons site provides this: submit your extension, go through review, and the distribution file is signed.

Mozilla’s review process has two tracks:

  • Listed. Full review, discoverable in the add-ons catalog. Some extensions go through human review; simpler ones may receive automated approval.
  • Unlisted. Signed but not listed. You distribute the file directly. Useful for internal tools.

Use web-ext sign to sign an extension via the Add-ons API without going through the web UI:

web-ext sign --api-key=$JWT_ISSUER --api-secret=$JWT_SECRET

FAQ

Should I develop for Firefox or Chrome first? If cross-browser support matters, develop for Firefox first and test in Chrome. Firefox’s more permissive MV3 implementation (maintaining webRequestBlocking) means code that works in Firefox might need adjustment for Chrome, but is less likely to be architecturally wrong. Going Chrome-first risks designing around Chrome’s MV3 restrictions and then discovering the extension can’t be adapted.

What’s the extension ID format in Firefox? Firefox extension IDs are UUIDs in curly braces: {a-b-c-d-e}, or can be email-formatted: myextension@example.com. Declare the ID in the manifest to get a stable ID across development reloads:

"browser_specific_settings": {
  "gecko": { "id": "myextension@example.com" }
}

How do I update the extension without reinstalling? web-ext run auto-reloads on file changes. For manual reload: about:debugging → “Reload” button next to the extension. Content scripts already injected into pages need the page to be refreshed to pick up the new version.

Can I use TypeScript? Yes. Use a bundler (webpack, esbuild, Vite) to compile TypeScript to JavaScript. The @types/webextension-polyfill package provides TypeScript types for the browser.* API. The bundled output goes in your extension directory.