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
- Navigate to
about:debugging#/runtime/this-firefox - Click "Load Temporary Add-on"
- Select the
manifest.jsonfile - 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.sendMessageandonMessagehandler - Storage not persisting: make sure you're using
storage.local(notstorage.sessionwhich 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.
Top comments (0)