Logo prmworks
Browser Extensions JavaScript Chrome Firefox

Building Modern Browser Extensions: A Comprehensive Guide

March 14, 2025 9 min read
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:

  1. Manifest file - Defines extension metadata and permissions
  2. Background scripts - Persistent scripts that manage the extension’s state
  3. Content scripts - Scripts that run in the context of web pages
  4. Popup UI - Optional user interface that appears when clicking the extension icon
  5. 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:

  1. Chrome: Go to chrome://extensions/, enable “Developer mode”, and click “Load unpacked” to select your extension directory.

  2. Brave: Go to brave://extensions/, enable “Developer mode”, and click “Load unpacked” to select your extension directory.

  3. Firefox: Go to about:debugging#/runtime/this-firefox, click “Load Temporary Add-on”, and select your manifest.json file.

  4. 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:

  1. Create a production build: Use webpack to bundle your code and generate optimized assets.

  2. Create a ZIP archive: Package your extension’s files for submission.

  3. Publish to stores:

Remember to create store listings with compelling descriptions, screenshots, and privacy policies.

Best Practices

To create successful browser extensions:

  1. Respect user privacy - Only request the permissions you actually need
  2. Optimize performance - Extensions run in the user’s browser, so efficiency matters
  3. Handle errors gracefully - Users have different configurations and environments
  4. Keep up with browser changes - Browser platforms evolve regularly
  5. Provide clear documentation - Help users understand how to use your extension
  6. 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!