DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

Building Firefox Extensions: WebExtensions API Getting Started Guide

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
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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"]
  }]
}
Enter fullscreen mode Exit fullscreen mode
// 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 });
});
Enter fullscreen mode Exit fullscreen mode

Background Scripts

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

// manifest.json
{
  "background": {
    "scripts": ["background.js"]
  }
}
Enter fullscreen mode Exit fullscreen mode
// 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);
  }
});
Enter fullscreen mode Exit fullscreen mode

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}`);
  }
});
Enter fullscreen mode Exit fullscreen mode

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
  ]
}
Enter fullscreen mode Exit fullscreen mode

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/*'
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)