JavaScriptTypeScript

Building Browser Extensions with JavaScript

Introduction

Browser extensions add powerful features directly to the user’s browser. From productivity tools and password managers to content blockers and developer utilities, extensions can modify pages, automate tasks, and integrate external services seamlessly. Because most modern browsers support the WebExtensions standard, JavaScript is the primary language for extension development, making it accessible to any web developer.

In this comprehensive guide, you will learn how browser extensions work under the hood, how to structure one from scratch using Manifest V3, and how to build secure, maintainable extensions that work across Chrome, Firefox, and Edge. By the end, you will have the knowledge to create and publish your own browser extension.

Why Build Browser Extensions

Extensions run close to the user and provide instant value without requiring server infrastructure. They are ideal for lightweight tools, workflow enhancements, and integrations that benefit from direct browser access.

  • Enhance websites without changing server code or waiting for site updates
  • Automate repetitive tasks like form filling, data extraction, or navigation
  • Add custom UI to web pages through overlays, sidebars, or injected elements
  • Integrate APIs and services directly into the browsing experience
  • Build cross-browser tools with one codebase using the WebExtensions standard

Extensions are a fast way to ship focused features that would otherwise require a full web application.

Understanding Browser Extension Architecture

Most modern browsers follow the WebExtensions standard, which defines a common API for extension development. This includes Chrome, Firefox, Edge, Brave, and Opera. While there are minor differences between browsers, the core concepts and APIs remain consistent.

A browser extension consists of several components that work together:

  • Manifest file: Configuration and metadata
  • Background script: Long-running logic and event handling
  • Content scripts: Code that runs inside web pages
  • Popup and options pages: User interface elements
  • Storage: Persistent data for settings and state

Understanding how these components communicate is essential for building effective extensions.

Project Structure

A well-organized extension follows a clear structure that separates concerns and makes the codebase maintainable.

my-extension/
├── manifest.json          # Extension configuration
├── background.js          # Service worker (Manifest V3)
├── content.js             # Content script for page injection
├── popup/
│   ├── popup.html         # Popup UI
│   ├── popup.css          # Popup styles
│   └── popup.js           # Popup logic
├── options/
│   ├── options.html       # Options page
│   └── options.js         # Options logic
├── icons/
│   ├── icon-16.png
│   ├── icon-48.png
│   └── icon-128.png
└── lib/
    └── utils.js           # Shared utilities

The Manifest File

The manifest.json file is the heart of every extension. It defines metadata, permissions, and which scripts to load. Manifest V3 is the current standard for Chrome and Edge, with Firefox also supporting it.

{
  "manifest_version": 3,
  "name": "Page Highlighter",
  "version": "1.0.0",
  "description": "Highlight and save text from any webpage",
  "icons": {
    "16": "icons/icon-16.png",
    "48": "icons/icon-48.png",
    "128": "icons/icon-128.png"
  },
  "action": {
    "default_popup": "popup/popup.html",
    "default_icon": {
      "16": "icons/icon-16.png",
      "48": "icons/icon-48.png"
    }
  },
  "permissions": [
    "storage",
    "activeTab"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "css": ["content.css"]
    }
  ],
  "options_page": "options/options.html"
}

Key manifest properties include:

  • manifest_version: Must be 3 for modern extensions
  • permissions: Capabilities the extension needs (storage, tabs, etc.)
  • action: Defines the toolbar icon and popup
  • background: Service worker for background processing
  • content_scripts: Scripts injected into web pages

Background Scripts (Service Workers)

In Manifest V3, background scripts run as service workers. They handle long-running logic, respond to browser events, and coordinate communication between extension components. Unlike V2 background pages, service workers are event-driven and do not persist indefinitely.

// background.js

// Run when extension is installed or updated
chrome.runtime.onInstalled.addListener((details) => {
  if (details.reason === 'install') {
    // Set default settings
    chrome.storage.local.set({
      enabled: true,
      highlightColor: '#ffff00',
      savedHighlights: []
    });
    console.log('Extension installed');
  }
});

// Listen for messages from content scripts or popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'saveHighlight') {
    saveHighlight(message.data).then(() => {
      sendResponse({ success: true });
    });
    return true; // Keep channel open for async response
  }

  if (message.action === 'getHighlights') {
    chrome.storage.local.get(['savedHighlights'], (result) => {
      sendResponse({ highlights: result.savedHighlights || [] });
    });
    return true;
  }
});

async function saveHighlight(highlight) {
  const { savedHighlights } = await chrome.storage.local.get(['savedHighlights']);
  const updated = [...(savedHighlights || []), highlight];
  await chrome.storage.local.set({ savedHighlights: updated });
}

// Listen for tab updates
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  if (changeInfo.status === 'complete' && tab.url) {
    // Notify content script that page loaded
    chrome.tabs.sendMessage(tabId, { action: 'pageLoaded' }).catch(() => {
      // Content script not ready yet, ignore
    });
  }
});

Service workers have important constraints to remember:

  • They can be terminated when idle and restarted on events
  • They cannot access the DOM directly
  • State must be persisted to storage rather than held in variables
  • Use chrome.alarms instead of setTimeout for scheduled tasks

Content Scripts

Content scripts run inside web pages and can interact with the DOM. They execute in an isolated world, meaning they share the DOM with the page but have separate JavaScript contexts. This prevents conflicts with page scripts while allowing full DOM access.

// content.js

// Listen for text selection
document.addEventListener('mouseup', handleSelection);

function handleSelection(event) {
  const selection = window.getSelection();
  const text = selection.toString().trim();

  if (text.length > 0) {
    showHighlightButton(event.clientX, event.clientY, text);
  }
}

function showHighlightButton(x, y, text) {
  // Remove existing button if present
  removeHighlightButton();

  const button = document.createElement('button');
  button.id = 'extension-highlight-btn';
  button.textContent = 'Highlight';
  button.style.cssText = `
    position: fixed;
    left: ${x}px;
    top: ${y + 10}px;
    z-index: 999999;
    padding: 8px 16px;
    background: #4285f4;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
  `;

  button.addEventListener('click', () => {
    highlightSelection(text);
    removeHighlightButton();
  });

  document.body.appendChild(button);

  // Remove button when clicking elsewhere
  document.addEventListener('mousedown', (e) => {
    if (e.target !== button) {
      removeHighlightButton();
    }
  }, { once: true });
}

function removeHighlightButton() {
  const existing = document.getElementById('extension-highlight-btn');
  if (existing) {
    existing.remove();
  }
}

async function highlightSelection(text) {
  const selection = window.getSelection();
  if (selection.rangeCount === 0) return;

  const range = selection.getRangeAt(0);
  const highlight = document.createElement('mark');
  highlight.className = 'extension-highlight';

  // Get highlight color from storage
  const { highlightColor } = await chrome.storage.local.get(['highlightColor']);
  highlight.style.backgroundColor = highlightColor || '#ffff00';

  range.surroundContents(highlight);

  // Save to storage via background script
  chrome.runtime.sendMessage({
    action: 'saveHighlight',
    data: {
      text,
      url: window.location.href,
      timestamp: Date.now()
    }
  });
}

// Listen for messages from background or popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'pageLoaded') {
    restoreHighlights();
  }
});

Popup UI

The popup appears when users click the extension icon in the toolbar. It provides quick access to extension features and settings. Popups are short-lived and close when users click elsewhere, so they should focus on immediate actions.

<!-- popup/popup.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    body {
      width: 300px;
      padding: 16px;
      font-family: system-ui, sans-serif;
    }
    h1 {
      font-size: 18px;
      margin: 0 0 16px 0;
    }
    .toggle {
      display: flex;
      align-items: center;
      gap: 8px;
      margin-bottom: 16px;
    }
    .highlights {
      max-height: 200px;
      overflow-y: auto;
    }
    .highlight-item {
      padding: 8px;
      border: 1px solid #ddd;
      border-radius: 4px;
      margin-bottom: 8px;
    }
  </style>
</head>
<body>
  <h1>Page Highlighter</h1>
  <div class="toggle">
    <input type="checkbox" id="enabled">
    <label for="enabled">Enable highlighting</label>
  </div>
  <h2>Recent Highlights</h2>
  <div class="highlights" id="highlights"></div>
  <script src="popup.js"></script>
</body>
</html>
// popup/popup.js

document.addEventListener('DOMContentLoaded', async () => {
  // Load current settings
  const { enabled } = await chrome.storage.local.get(['enabled']);
  document.getElementById('enabled').checked = enabled;

  // Load recent highlights
  const response = await chrome.runtime.sendMessage({ action: 'getHighlights' });
  renderHighlights(response.highlights);

  // Handle toggle
  document.getElementById('enabled').addEventListener('change', async (e) => {
    await chrome.storage.local.set({ enabled: e.target.checked });
  });
});

function renderHighlights(highlights) {
  const container = document.getElementById('highlights');

  if (highlights.length === 0) {
    container.innerHTML = '<p>No highlights yet</p>';
    return;
  }

  const recent = highlights.slice(-5).reverse();
  container.innerHTML = recent.map(h => `
    <div class="highlight-item">
      <div class="highlight-text">"${escapeHtml(h.text.substring(0, 100))}..."</div>
      <div class="highlight-url">${escapeHtml(new URL(h.url).hostname)}</div>
    </div>
  `).join('');
}

function escapeHtml(text) {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

Extension Storage

Extensions have access to dedicated storage APIs that persist data across browser sessions. There are two main storage areas:

// Local storage - device-specific
await chrome.storage.local.set({ key: 'value' });
const { key } = await chrome.storage.local.get(['key']);

// Sync storage - syncs across devices when user is signed in
await chrome.storage.sync.set({ preferences: { theme: 'dark' } });
const { preferences } = await chrome.storage.sync.get(['preferences']);

// Listen for storage changes
chrome.storage.onChanged.addListener((changes, areaName) => {
  if (areaName === 'local' && changes.enabled) {
    console.log('Enabled changed:', changes.enabled.newValue);
  }
});

Storage limits apply: local storage allows up to 10MB, while sync storage is limited to 100KB total with 8KB per item.

Messaging Between Components

Extension components communicate through the messaging API. This enables content scripts, background scripts, and popup pages to coordinate actions.

// Send message from content script to background
chrome.runtime.sendMessage({ action: 'getData' }, (response) => {
  console.log('Received:', response);
});

// Send message from background to specific tab
chrome.tabs.sendMessage(tabId, { action: 'update' });

// Long-lived connections for ongoing communication
const port = chrome.runtime.connect({ name: 'sidebar' });
port.postMessage({ action: 'init' });
port.onMessage.addListener((message) => {
  console.log('Received:', message);
});

Permissions and Security

Permissions define what your extension can access. Request only what you need, as excessive permissions reduce user trust and may block store approval.

  • activeTab: Access to current tab when user invokes extension (preferred over broad host permissions)
  • storage: Access to extension storage APIs
  • tabs: Read tab URLs and metadata
  • scripting: Programmatically inject scripts
  • Host permissions: Access to specific domains
{
  "permissions": ["storage", "activeTab"],
  "optional_permissions": ["tabs"],
  "host_permissions": ["*://*.github.com/*"]
}

Use optional permissions when features are not always needed. Users grant these on demand, improving trust.

Handling Browser Differences

Although WebExtensions are standardized, small differences exist between browsers. Chrome uses callback-based APIs while Firefox supports Promises natively. The webextension-polyfill library provides a unified Promise-based API.

// Without polyfill - Chrome style
chrome.storage.local.get(['key'], (result) => {
  console.log(result.key);
});

// With polyfill - Promise style (works everywhere)
import browser from 'webextension-polyfill';

const result = await browser.storage.local.get(['key']);
console.log(result.key);

Test on multiple browsers before publishing. Common differences include notification APIs, context menu behavior, and manifest property support.

Debugging Extensions

Browser developer tools provide excellent debugging support for extensions:

  • Background script: Open chrome://extensions, find your extension, click “Inspect views: service worker”
  • Content scripts: Use the Sources panel in page DevTools, find your script under Content Scripts
  • Popup: Right-click the popup and select “Inspect” to open DevTools
  • Storage: Use the Application panel to view extension storage

Always load extensions in developer mode during development. This enables auto-reload on file changes in some browsers.

When to Build Browser Extensions

Browser extensions are the right choice in specific scenarios. Understanding when they fit helps you avoid overengineering or choosing the wrong approach.

Extensions Are Ideal For

  • Website enhancement: Adding features to sites you do not control
  • Developer tools: Inspectors, debuggers, and productivity aids
  • Content modification: Ad blockers, readability improvements, translation
  • Quick automation: Form filling, screenshot capture, data extraction
  • Cross-site integrations: Connecting services across different websites

Consider Alternatives When

  • Complex applications: Full web apps with frameworks like Next.js are better for rich functionality
  • Mobile support needed: Extensions are desktop-only; consider mobile frameworks instead
  • Server-side processing: Extensions run client-side; use proper backends for heavy computation
  • Broad distribution: App stores have wider reach than extension stores

Common Mistakes to Avoid

Overusing Permissions

Requesting broad host permissions when activeTab would suffice reduces user trust and complicates store review.

Running Heavy Logic in Content Scripts

Content scripts run on every page load. Heavy computation slows down the user’s browsing experience. Move intensive work to the background script.

Ignoring Service Worker Lifecycle

Service workers can terminate at any time. Store state persistently and design for restart scenarios.

Publishing Your Extension

Each browser has its own extension store with specific requirements:

  • Chrome Web Store: $5 one-time developer fee, review process takes days
  • Firefox Add-ons: Free, generally faster review
  • Microsoft Edge Add-ons: Free, accepts Chrome extensions with minimal changes

Prepare promotional assets including icons, screenshots, and descriptions. Follow each store’s content policy to avoid rejection.

Conclusion

Building browser extensions with JavaScript allows you to create powerful tools that run directly inside the browser. By understanding extension architecture, the manifest file, content scripts, background service workers, and messaging between components, you can build secure and efficient extensions that work across modern browsers. The WebExtensions standard makes cross-browser development practical, while Manifest V3 provides a secure foundation for modern extensions.

For frontend JavaScript patterns that apply to extension development, read Modern ECMAScript Features You Might Have Missed. To understand async patterns used in extension APIs, see JavaScript Promises and Async/Await Error Handling Patterns. For debugging techniques, explore Mastering Debugging in VS Code and IntelliJ IDEA. Reference the official Chrome extension documentation and the MDN WebExtensions guide for the latest APIs and best practices.

Leave a Comment