Building Firefox extensions has become much more approachable. Here's a practical getting-started guide using the WebExtensions API.

Getting Started: Your First Extension

Minimum required files:

my-extension/ manifest.json popup.html popup.js 

manifest.json:

{ "manifest_version": 2, "name": "My Extension", "version": "1.0", "description": "Does something useful", "permissions": ["activeTab"], "browser_action": { "default_popup": "popup.html", "default_title": "My Extension", "default_icon": { "48": "icons/icon-48.png" } } } 

popup.html:

<!DOCTYPE html> <html> <head><meta charset="utf-8"></head> <body> <button id="doSomething">Do Something</button> <p id="result"></p> <script src="popup.js"></script> </body> </html> 

popup.js:

document.getElementById('doSomething').addEventListener('click', async () => { const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); document.getElementById('result').textContent = 'URL: ' + tab.url; }); 

Loading in Firefox for Development

  1. Navigate to about:debugging#/runtime/this-firefox
  2. Click "Load Temporary Add-on"
  3. Select the manifest.json file
  4. Your extension appears in the list

The extension stays loaded until Firefox restarts. Changes to files reload when you click "Reload" on the extension.

Inspect the extension: In about:debugging, click "Inspect" next to your extension to open DevTools for the background page/popup.

Content Scripts

Content scripts run in the context of web pages:

// manifest.json { "content_scripts": [{ "matches": ["https://example.com/*"], "js": ["content.js"] }] } 
// content.js — runs on every matching page console.log('Page title:', document.title); // Inject a button const btn = document.createElement('button'); btn.textContent = 'My Button'; document.body.prepend(btn); btn.addEventListener('click', () => { // Communicate with background script browser.runtime.sendMessage({ type: 'button-clicked', title: document.title }); }); 

Background Scripts

Background scripts run persistently (or event-based in MV3):

// manifest.json { "background": { "scripts": ["background.js"] } } 
// background.js browser.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'button-clicked') { console.log('Button clicked on:', message.title); // Could save to storage, open a tab, make an API call, etc. } }); // Listen for tab changes browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (changeInfo.status === 'complete') { console.log('Tab completed loading:', tab.url); } }); 

Storage API

// Save data await browser.storage.local.set({ counter: 42, settings: { theme: 'dark' } }); // Read data const result = await browser.storage.local.get('counter'); console.log(result.counter); // 42 // Read multiple keys const data = await browser.storage.local.get(['counter', 'settings']); // Listen for changes browser.storage.onChanged.addListener((changes, area) => { for (const [key, { oldValue, newValue }] of Object.entries(changes)) { console.log(`${key}: ${oldValue} → ${newValue}`); } }); 

Permissions: Request What You Need

Common permissions and what they allow:

{ "permissions": [ "activeTab", // Access current tab (on user action only) "tabs", // Access all tabs, URLs, titles "storage", // browser.storage.local and .sync "history", // browser.history "bookmarks", // browser.bookmarks "notifications", // browser.notifications "webRequest", // Intercept network requests (MV2) "<all_urls>", // Content scripts on all URLs "https://api.example.com/*" // Specific origin access ] } 

Request the minimum permissions your extension needs. Users see what permissions are requested at install time. Fewer permissions = more trust.

Debugging

Background page DevTools: about:debugging → Inspect → Full DevTools for background context.

Content script debugging: Open DevTools on the page where your content script runs → Console → see your content script's logs.

Popup debugging: Right-click the extension icon → Inspect Popup → DevTools for the popup.

Common mistakes to debug:

  • Content script messages not reaching background: check browser.runtime.sendMessage and onMessage handler
  • Storage not persisting: make sure you're using storage.local (not storage.session which is tab-scoped)
  • API permission denied: add the required permission to manifest.json

Packaging for AMO Submission

# Create a zip of the extension cd my-extension zip -r ../my-extension.zip . -x '*.git*' -x 'node_modules/*' 

Submit at addons.mozilla.org → Developer Hub → Submit a New Add-on.

AMO requirements:

  • No obfuscated code (all code must be readable)
  • No remote code execution (no eval() of remote strings)
  • Justification for each permission requested
  • Privacy policy if you collect any user data

Review time: typically 1-7 days for unlisted (self-distribution), longer for listed extensions.

Moving to Manifest V3

Firefox supports MV3 while maintaining blocking webRequest. For new extensions, MV3 is the forward-compatible choice:

{ "manifest_version": 3, "action": { // Changed from browser_action "default_popup": "popup.html" }, "background": { "service_worker": "background.js" // Service worker instead of script } } 

In MV3, background scripts are service workers — they don't run persistently. Use chrome.storage for state that persists between activations.

The transition affects architecture but the core browser APIs remain the same.