DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

Building Firefox Extensions: A Practical Start with Manifest V3

Building a Firefox extension is more approachable than it looks. Here's a practical start with the actual APIs and gotchas.

The Basics: Manifest V2 vs V3

Firefox supports both Manifest V2 (MV2) and Manifest V3 (MV3) extensions. Unlike Chrome, Firefox's MV3 still supports the blocking webRequest API (declarativeNetRequest is available too, but not forced). For new extensions, use MV3 — it's the forward-compatible choice.

Minimum manifest.json:

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0.0",
  "description": "What it does",
  "action": {
    "default_popup": "popup.html",
    "default_icon": "icon48.png"
  },
  "permissions": ["storage", "tabs"],
  "background": {
    "service_worker": "background.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

Extension Components

Background script (background.js / service worker): Runs in the background, handles events. On MV3, it's a service worker (can terminate when idle, no persistent state without using storage).

Content scripts: Run in the context of web pages. Can access the DOM, but not privileged browser APIs. Communicate with background script via chrome.runtime.sendMessage().

Popup (popup.html): The UI shown when the user clicks the extension icon. Standard HTML/CSS/JS. Has access to privileged APIs.

Options page: Settings UI for the extension. Defined in options_ui.page in the manifest.

Important APIs

Storage:

// Store
await chrome.storage.local.set({ key: 'value' });

// Retrieve
const result = await chrome.storage.local.get(['key']);
console.log(result.key);

// Listen for changes
chrome.storage.onChanged.addListener((changes, area) => {
  if (area === 'local' && changes.key) {
    console.log('key changed:', changes.key.newValue);
  }
});
Enter fullscreen mode Exit fullscreen mode

Tabs:

// Get all tabs
const tabs = await chrome.tabs.query({});

// Get current tab
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });

// Create a tab
await chrome.tabs.create({ url: 'https://example.com' });

// Execute script in a tab
await chrome.scripting.executeScript({
  target: { tabId: tab.id },
  func: () => document.title
});
Enter fullscreen mode Exit fullscreen mode

Messaging (background ↔ popup ↔ content script):

// From popup/content to background:
const response = await chrome.runtime.sendMessage({ type: 'GET_DATA' });

// In background, handle it:
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === 'GET_DATA') {
    sendResponse({ data: 'hello' });
    return true; // keep channel open for async response
  }
});
Enter fullscreen mode Exit fullscreen mode

Loading Your Extension in Firefox

  1. Go to about:debugging
  2. "This Firefox" → "Load Temporary Add-on…"
  3. Select your manifest.json

The extension loads without signing. You can edit the source and reload from about:debugging.

For permanent loading (without going through AMO), use a Developer Build of Firefox or sign via AMO's unlisted option.

Permissions: Ask for Minimum

Request only the permissions you need. Overly broad permissions (<all_urls>, tabs without reason) slow Firefox Add-on review. The review team scrutinizes permissions carefully.

Common narrow permissions:

  • activeTab — access to the current tab only, when the user invokes the extension
  • storage — local storage access
  • alarms — timer API
  • notifications — system notifications
  • scripting — required for chrome.scripting.executeScript()

Host permissions (e.g., "*://example.com/*") require explicit declaration if you need cross-origin access from a content script.

Content Security Policy

Firefox extensions have a default CSP that blocks eval, inline scripts, and remote code loading. This is intentional. Write all your JS in separate .js files, not inline <script> tags.

For external resources in the popup HTML (e.g., fonts from Google Fonts), you need to either bundle them locally or update your manifest's CSP — AMO reviewers may scrutinize this.

Debugging

  • Background script: about:debugging → Your Extension → "Inspect"
  • Popup: right-click the popup → "Inspect"
  • Content script: regular page DevTools → Console shows content script logs

Publishing on AMO

  1. Create a developer account at addons.mozilla.org
  2. Submit your extension ZIP file
  3. Human review for new extensions (can take 1-7 days for first submission)
  4. After approval, automatic review for subsequent updates (usually minutes for code-only changes)

AMO requires source code submission for obfuscated bundles. If you use webpack/rollup, upload the source alongside the bundle.

The Gecko Advantage

One reason to build for Firefox specifically: your extension can still use the full webRequest blocking API, which Chrome restricts in MV3. If your extension does content blocking, filtering, or request interception — Firefox is the better platform for it.


The hardest part of extension development isn't the code — it's AMO review for new extensions. The review process is thorough (human reviewers check the source code) but that same process makes AMO extensions more trustworthy than the Chrome Web Store.

Top comments (0)