Creating a browser extension is a fun way to add new features to your web browser, like changing how pages look, adding buttons, or automating tasks. This guide will walk you through how to build a basic extension that works in both Chrome and Firefox.
Project Structure
my-extension/
├── manifest.json
├── icons/
│ ├── icon16.png
│ ├── icon48.png
│ └── icon128.png
├── popup/
│ ├── popup.html
│ ├── popup.css
│ └── popup.js
├── background.js
├── content.js
└── polyfill.js ← optional but recommendedManifest File (manifest.json)
{
"manifest_version": 3,
"name": "My Cross-Browser Extension",
"version": "1.0",
"description": "A correct, Manifest V3 extension for Chrome and Firefox.",
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"permissions": [],
"host_permissions": [],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png"
}
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"web_accessible_resources": [
{
"resources": ["popup/popup.html", "popup/popup.js"],
"matches": ["<all_urls>"]
}
]
}Key Notes:
default_popupis set →action.onClickedwill not fire on icon click.- If you want to listen to clicks, remove
default_popupand usechrome.action.onClicked. web_accessible_resourceslists specific files, not wildcards.
Popup UI (popup/popup.html, popup.css, popup.js)
popup/popup.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="popup.css" />
<!-- Optional: Load polyfill if using webextension-polyfill -->
<script src="../polyfill.js"></script>
</head>
<body>
<div class="container">
<h1>Extension Popup</h1>
<button id="greet">Say Hello</button>
<button id="triggerBg">Trigger Background</button>
</div>
<script src="popup.js"></script>
</body>
</html>popup/popup.css
body {
width: 300px;
padding: 16px;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.container {
text-align: center;
}
button {
margin: 8px;
padding: 8px 16px;
font-size: 14px;
}popup/popup.js
document.getElementById('greet').addEventListener('click', () => {
// ✅ Safe: alert works in popup context
alert('Hello from the popup!');
});
document.getElementById('triggerBg').addEventListener('click', () => {
// Send message to background script
browser.runtime.sendMessage({ type: 'open_popup' });
});Background Service Worker (background.js)
No DOM access here! Use chrome.scripting.executeScript or open a popup instead of alert().
// Listen for messages from popup
browser.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'open_popup') {
// Open the popup programmatically (only works if no default_popup is set)
// But since we have default_popup, this won't open it automatically.
// Instead, you could open a new window or do background logic.
console.log('Popup requested via background script.');
}
});
// Optional: Listen to install
browser.runtime.onInstalled.addListener(() => {
console.log('Extension installed.');
});To receive icon clicks, remove "default_popup" and use:
browser.action.onClicked.addListener((tab) => {
browser.action.openPopup(); // or do something else
});Content Script (content.js)
// Runs in the context of web pages
document.body.style.backgroundColor = "#f0f8ff";Cross-Browser Compatibility: Use webextension-polyfill
Install via npm:
npm install webextension-polyfillOr download from GitHub.
In your scripts (e.g., popup.js, background.js):
import browser from 'webextension-polyfill';
browser.runtime.onMessage.addListener(...);
browser.action.openPopup();Include the polyfill in your HTML if not bundling:
<script src="polyfill.js"></script>Testing Your Extension
For Firefox: Use web-ext
- Install
web-ext:
npm install -g web-ext- Run:
web-ext runFirefox opens with your extension loaded and auto-reloads on changes.
For Chrome
- Go to
chrome://extensions/ - Enable Developer mode
- Click Load unpacked
- Select your extension folder
When You Want to Listen to Icon Clicks (No Popup)
Remove "default_popup" from manifest.json, then use:
// background.js
browser.action.onClicked.addListener((tab) => {
// Open a new popup window manually
browser.windows.create({
url: browser.runtime.getURL('popup/popup.html'),
type: 'popup',
width: 320,
height: 240
});
});Now onClicked will fire and you can control the popup behavior.
Showing UI from Background Script (No Popup)
If you can’t use a popup, inject a script into the page:
browser.action.onClicked.addListener((tab) => {
browser.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
alert('Hello from background via injected script!');
}
});
});Avoid alert() in production. Use a content script to inject a custom UI instead.
Publishing to Stores
- Chrome Web Store:
Package your folder as a.zipand upload to Chrome Developer Dashboard. - Firefox Add-ons:
Use AMO Developer Hub to submit your.zip.
Best Practices Summary
| Practice | Why |
|---|---|
Use webextension-polyfill | Write once, run on both browsers |
Avoid alert() in background | Use content scripts or popup |
List exact files in web_accessible_resources | Security and clarity |
Test with web-ext and Chrome dev tools | Catch browser-specific issues early |
Prefer browser.* over chrome.* | Future-proof and cross-browser |
Remove default_popup if you need onClicked | Avoid confusion |



