Building Modern Browser Extensions: A Comprehensive Guide

Building Modern Browser Extensions: A Comprehensive Guide
Browser extensions can enhance the browsing experience, improve productivity, and solve specific problems for users. In this comprehensive guide, we’ll explore how to build, test, and publish modern browser extensions that work across multiple browsers.
Understanding Browser Extensions
Browser extensions are software programs that extend the functionality of web browsers. They can modify web content, add new features to websites, or provide utility functions accessible from the browser’s toolbar.
Modern extensions typically consist of:
- Manifest file - Defines extension metadata and permissions
- Background scripts - Persistent scripts that manage the extension’s state
- Content scripts - Scripts that run in the context of web pages
- Popup UI - Optional user interface that appears when clicking the extension icon
- Options page - Settings page for the extension
Setting Up Your Development Environment
Before we start coding, let’s set up a development environment:
# Create project directory
mkdir my-extension
cd my-extension
# Initialize package.json
npm init -y
# Install development dependencies
npm install --save-dev webpack webpack-cli copy-webpack-plugin
Next, let’s create a basic directory structure:
my-extension/
├── src/
│ ├── manifest.json
│ ├── background/
│ │ └── index.js
│ ├── content/
│ │ └── index.js
│ ├── popup/
│ │ ├── popup.html
│ │ └── popup.js
│ └── options/
│ ├── options.html
│ └── options.js
├── public/
│ └── icons/
│ ├── icon-16.png
│ ├── icon-48.png
│ └── icon-128.png
└── package.json
Creating the Manifest File
The manifest.json file is the blueprint of your extension. Let’s create a basic manifest that works with Manifest V3, the latest standard: Firefox and Edge also support Manifest V3, but some features may vary slightly.
{
"manifest_version": 3,
"name": "My Browser Extension",
"version": "1.0.0",
"description": "A helpful browser extension",
"icons": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
},
"action": {
"default_popup": "popup/popup.html",
"default_title": "My Extension"
},
"background": {
"service_worker": "background/index.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content/index.js"]
}
],
"permissions": ["storage", "activeTab"],
"options_page": "options/options.html"
}
This manifest sets up a basic extension with background services, content scripts, and a popup interface.
Implementing Background Scripts
Background scripts run separately from web pages and manage the extension’s state. Here’s a simple background script that listens for installation and messages:
// src/background/index.js
// Handle extension installation
chrome.runtime.onInstalled.addListener(({ reason }) => {
if (reason === "install") {
// Initialize extension state
chrome.storage.local.set({
isEnabled: true,
settings: {
theme: "light",
notifications: true
}
});
// Open options page after installation
chrome.runtime.openOptionsPage();
}
});
// Listen for messages from content scripts or popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "GET_STATE") {
chrome.storage.local.get(["isEnabled", "settings"], (result) => {
sendResponse(result);
});
return true; // Required for async response
}
if (message.type === "TOGGLE_ENABLED") {
chrome.storage.local.get("isEnabled", ({ isEnabled }) => {
chrome.storage.local.set({ isEnabled: !isEnabled }, () => {
sendResponse({ isEnabled: !isEnabled });
});
});
return true; // Required for async response
}
});
Creating Content Scripts
Content scripts run in the context of web pages, allowing you to read and modify page content:
// src/content/index.js
// Check if extension is enabled before running
chrome.runtime.sendMessage({ type: "GET_STATE" }, (response) => {
if (response && response.isEnabled) {
initializeExtension(response.settings);
}
});
function initializeExtension(settings) {
console.log("Extension initialized with settings:", settings);
// Example: Add a custom style to the page based on theme setting
if (settings.theme === "dark") {
const style = document.createElement("style");
style.textContent = `
body {
background-color: #222 !important;
color: #eee !important;
}
a {
color: #5af !important;
}
`;
document.head.appendChild(style);
}
// Example: Add a floating button to the page
const button = document.createElement("button");
button.textContent = "Extension Action";
button.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
padding: 10px 15px;
background: #4285f4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
`;
button.addEventListener("click", () => {
// Send message to background script
chrome.runtime.sendMessage({
type: "BUTTON_CLICKED",
data: { url: window.location.href }
});
});
document.body.appendChild(button);
}
Building the Popup UI
The popup is the interface users see when clicking your extension icon:
<!-- src/popup/popup.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Extension</title>
<style>
body {
width: 300px;
padding: 15px;
font-family: system-ui, -apple-system, sans-serif;
}
h1 {
font-size: 18px;
margin-top: 0;
}
.toggle {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 40px;
height: 24px;
margin-right: 10px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: 0.4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #4285f4;
}
input:checked + .slider:before {
transform: translateX(16px);
}
.footer {
margin-top: 15px;
display: flex;
justify-content: space-between;
}
</style>
</head>
<body>
<h1>My Browser Extension</h1>
<div class="toggle">
<label class="toggle-switch">
<input type="checkbox" id="enabled-toggle" />
<span class="slider"></span>
</label>
<span>Enable extension</span>
</div>
<div id="status-message"></div>
<div class="footer">
<button id="options-button">Options</button>
<span id="version">v1.0.0</span>
</div>
<script src="popup.js"></script>
</body>
</html>
// src/popup/popup.js
// Get UI elements
const enabledToggle = document.getElementById("enabled-toggle");
const statusMessage = document.getElementById("status-message");
const optionsButton = document.getElementById("options-button");
const versionElement = document.getElementById("version");
// Set version from manifest
chrome.runtime.getManifest().version;
versionElement.textContent = `v${chrome.runtime.getManifest().version}`;
// Get initial state
chrome.runtime.sendMessage({ type: "GET_STATE" }, (response) => {
if (response) {
enabledToggle.checked = response.isEnabled;
updateStatusMessage(response.isEnabled);
}
});
// Toggle enabled state
enabledToggle.addEventListener("change", () => {
chrome.runtime.sendMessage({ type: "TOGGLE_ENABLED" }, (response) => {
updateStatusMessage(response.isEnabled);
});
});
// Open options page
optionsButton.addEventListener("click", () => {
chrome.runtime.openOptionsPage();
});
// Update status message based on enabled state
function updateStatusMessage(isEnabled) {
statusMessage.textContent = isEnabled
? "Extension is currently active on this site."
: "Extension is disabled. Toggle to enable.";
statusMessage.style.color = isEnabled ? "#4285f4" : "#999";
}
Creating an Options Page
The options page lets users customize the extension:
<!-- src/options/options.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Extension Options</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
h1 {
font-size: 24px;
margin-bottom: 20px;
}
.option-group {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #eee;
border-radius: 5px;
}
h2 {
font-size: 18px;
margin-top: 0;
}
label {
display: block;
margin-bottom: 10px;
}
select,
input[type="text"] {
padding: 8px;
border-radius: 4px;
border: 1px solid #ddd;
width: 300px;
max-width: 100%;
}
.buttons {
margin-top: 20px;
}
button {
padding: 8px 16px;
background: #4285f4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
button.secondary {
background: #f1f1f1;
color: #333;
}
.saved-message {
color: green;
margin-top: 15px;
opacity: 0;
transition: opacity 0.3s;
}
.saved-message.visible {
opacity: 1;
}
</style>
</head>
<body>
<h1>Extension Options</h1>
<div class="option-group">
<h2>Appearance</h2>
<label>
Theme:
<select id="theme-select">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">Follow System</option>
</select>
</label>
</div>
<div class="option-group">
<h2>Notifications</h2>
<label>
<input type="checkbox" id="notifications-checkbox" />
Enable notifications
</label>
</div>
<div class="buttons">
<button id="save-button">Save Changes</button>
<button id="reset-button" class="secondary">Reset to Defaults</button>
</div>
<div id="saved-message" class="saved-message">
Settings saved successfully!
</div>
<script src="options.js"></script>
</body>
</html>
// src/options/options.js
// Get UI elements
const themeSelect = document.getElementById("theme-select");
const notificationsCheckbox = document.getElementById("notifications-checkbox");
const saveButton = document.getElementById("save-button");
const resetButton = document.getElementById("reset-button");
const savedMessage = document.getElementById("saved-message");
// Load current settings
loadSettings();
// Save settings
saveButton.addEventListener("click", () => {
const settings = {
theme: themeSelect.value,
notifications: notificationsCheckbox.checked
};
chrome.storage.local.set({ settings }, () => {
showSavedMessage();
});
});
// Reset to defaults
resetButton.addEventListener("click", () => {
const defaultSettings = {
theme: "light",
notifications: true
};
chrome.storage.local.set({ settings: defaultSettings }, () => {
loadSettings();
showSavedMessage();
});
});
// Load settings from storage
function loadSettings() {
chrome.storage.local.get("settings", (result) => {
const settings = result.settings || {
theme: "light",
notifications: true
};
themeSelect.value = settings.theme;
notificationsCheckbox.checked = settings.notifications;
});
}
// Show saved message
function showSavedMessage() {
savedMessage.classList.add("visible");
setTimeout(() => {
savedMessage.classList.remove("visible");
}, 3000);
}
Testing Your Extension
To test your extension during development:
-
Chrome: Go to
chrome://extensions/
, enable “Developer mode”, and click “Load unpacked” to select your extension directory. -
Brave: Go to
brave://extensions/
, enable “Developer mode”, and click “Load unpacked” to select your extension directory. -
Firefox: Go to
about:debugging#/runtime/this-firefox
, click “Load Temporary Add-on”, and select your manifest.json file. -
Edge: Go to
edge://extensions/
, enable “Developer mode”, and click “Load unpacked” to select your extension directory.
During testing, take advantage of the browser’s developer tools. You can access your background script’s console by clicking “inspect” on the extension’s card in the extensions management page.
Packaging and Publishing
When your extension is ready for distribution:
-
Create a production build: Use webpack to bundle your code and generate optimized assets.
-
Create a ZIP archive: Package your extension’s files for submission.
-
Publish to stores:
- Chrome and Brave Web Store: Developer Dashboard
- Firefox Add-ons: AMO Developer Hub
- Microsoft Edge Add-ons: Partner Center
Remember to create store listings with compelling descriptions, screenshots, and privacy policies.
Best Practices
To create successful browser extensions:
- Respect user privacy - Only request the permissions you actually need
- Optimize performance - Extensions run in the user’s browser, so efficiency matters
- Handle errors gracefully - Users have different configurations and environments
- Keep up with browser changes - Browser platforms evolve regularly
- Provide clear documentation - Help users understand how to use your extension
- Implement analytics carefully - Be transparent about data collection
Conclusion
Building browser extensions is a powerful way to enhance the web experience. With modern extension APIs and good development practices, you can create tools that integrate seamlessly with browsers and provide real value to users.
Have you built any browser extensions? What challenges did you face? Let me know in the comments below!
Related Posts

Create Your First MCP Server with Node.js
May 12, 2025
Learn how to build a Model Context Protocol server using Node.js to integrate arithmetic functions with AI assistants.

Modern Web Development: Trends and Best Practices for 2025
April 19, 2025
Explore the latest trends and best practices shaping web development in 2025, from advanced frameworks to performance optimization techniques.