The Ultimate Guide to Building a Browser Extension from Scratch: A Step-by-Step Tutorial for Beginners and Pros

Browser extensions have become an indispensable part of modern web browsing, enabling users to customize their online experience, improve productivity, enhance security, and access specialized tools directly from their browser toolbar. Whether you dream of creating a simple ad blocker, a note-taking assistant, a password manager, or a complex automation tool, learning how to build a browser extension is a valuable skill that opens up a world of possibilities. In this comprehensive tutorial, we will walk you through every single stage of the process, from understanding the core architecture of a browser extension to deploying your finished product on the Chrome Web Store or other marketplaces. By the end of this article, you will have a solid foundation to build, test, and publish your own extension, and you’ll be equipped with best practices that ensure performance, security, and user satisfaction. We’ll use Google Chrome as our primary development platform, but the concepts apply to most Chromium-based browsers (Edge, Brave, Opera) and, with minor adjustments, to Firefox as well.

Before we dive into the technical details, it’s important to grasp what a browser extension really is. At its core, a browser extension is a small software program that modifies the browser’s functionality. It typically consists of HTML, CSS, and JavaScript files, packed together with a metadata file called manifest.json. The extension can interact with web pages, browser tabs, bookmarks, history, storage, and even the operating system through a set of APIs provided by the browser. Extensions live in a sandboxed environment, meaning they have limited privileges but can request specific permissions to access user data or alter web content. This architecture ensures that extensions are safe for users while still being powerful. Now, let’s get our hands dirty and build an extension from nothing.

Article illustration

Understanding the Anatomy of a Browser Extension

Before writing a single line of code, you need to understand the structure of an extension. Every extension has a root directory containing at least a manifest.json file. This JSON file is the heart of your extension; it tells the browser about the extension’s name, version, permissions, background scripts, content scripts, popup pages, and other resources. The manifest must be valid JSON, and the manifest_version property is critical. As of 2025, manifest_version 3 is the standard for Chrome and most Chromium browsers, replacing the older version 2 which is being phased out. Manifest v3 introduces service workers instead of persistent background pages, new security models, and changes to how APIs are used. We will focus on manifest v3 throughout this tutorial because it is the future of browser extension development.

Apart from manifest.json, a typical extension may include a background.js (or service-worker.js) for handling events, a popup.html and popup.js for the toolbar popup interface, content.js for scripts that run on web pages, and style.css for styling. You may also have icons in PNG format, an options.html for settings, and other assets. The exact files vary depending on the extension’s purpose. The key is to keep your code organized and modular. In the next sections, we’ll build a simple extension that changes the background color of any webpage when you click a button in the popup. This will teach you the fundamentals: manifest declaration, popup UI, content scripts, and message passing.

Step-by-Step Guide: Building Your First Browser Extension

Step 1: Set Up the Project Folder and Create the Manifest

Create a new folder on your computer, for example my-extension. Inside, create a file named manifest.json. Open it in your favorite code editor (VS Code is highly recommended). Write the following JSON content. This is the minimal manifest for a manifest v3 extension with a popup and a content script:

{
  "manifest_version": 3,
  "name": "Page Color Changer",
  "version": "1.0",
  "description": "Changes the background color of any webpage to a color of your choice.",
  "permissions": ["storage", "activeTab"],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icon16.png",
      "48": "icon48.png",
      "128": "icon128.png"
    }
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"]
    }
  ],
  "background": {
    "service_worker": "background.js"
  },
  "icons": {
    "16": "icon16.png",
    "48": "icon48.png",
    "128": "icon128.png"
  }
}

Let’s break down each field. manifest_version must be 3. name and version are self-explanatory. description is optional but recommended. permissions is an array; we request storage (to save user preferences) and activeTab (to access the currently active tab temporarily). The action object defines the toolbar button behavior, including the popup HTML file. content_scripts specifies scripts that run in the context of web pages; we set matches to <all_urls> so it runs on every page. The background property with a service_worker tells Chrome to use a service worker (non‑persistent) for background tasks. Finally, icons are required for the browser’s management page. You’ll need to create simple icon files (or use placeholder images). For quick testing, you can just create a 16×16 PNG file with a solid color using any image editor or even a paint app. Place these icons in the same folder.

Important: The manifest is case‑sensitive. Any typo will prevent the extension from loading. Also, make sure your JSON is valid—you can use an online validator if needed. Once the manifest and icons are ready, you have the skeleton of your extension.

Step 2: Create the Popup HTML and JavaScript

The popup is the small window that appears when the user clicks your extension’s toolbar icon. Create a file named popup.html in your project folder. Write the following HTML:

<!DOCTYPE html>
<html>
<head>
  <style>
    body { width: 200px; padding: 10px; font-family: Arial, sans-serif; }
    button { display: block; margin: 10px 0; padding: 8px 16px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; }
    input { width: 100%; padding: 5px; margin: 5px 0; box-sizing: border-box; }
  </style>
</head>
<body>
  <h3>Change Background Color</h3>
  <label for="color">Choose a color:</label>
  <input type="color" id="color" value="#ff0000" />
  <button id="applyBtn">Apply Color</button>
  <button id="resetBtn">Reset to Default</button>
  <script src="popup.js"></script>
</body>
</html>

This popup provides a color picker input and two buttons. Now create popup.js with the logic that sends messages to the content script. Because the popup runs in its own isolated context, it cannot directly modify the web page. It must send a message to the content script running on the active tab.

document.getElementById('applyBtn').addEventListener('click', () => {
  const color = document.getElementById('color').value;
  chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
    chrome.tabs.sendMessage(tabs[0].id, { action: 'changeColor', color: color });
  });
});

document.getElementById('resetBtn').addEventListener('click', () => {
  chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
    chrome.tabs.sendMessage(tabs[0].id, { action: 'resetColor' });
  });
});

We use chrome.tabs.query to get the current active tab. Then we call chrome.tabs.sendMessage with an object containing an action string and the color value. The content script will listen for these messages.

Step 3: Create the Content Script

The content script runs on every page that matches the patterns defined in manifest.json‘s content_scripts array. Create content.js in your folder. This script will listen for messages from the popup (or background) and change the page’s background color accordingly.

// Listen for messages from the popup or background script
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'changeColor') {
    document.body.style.backgroundColor = request.color;
  } else if (request.action === 'resetColor') {
    document.body.style.backgroundColor = ''; // Revert to original
  }
});

That’s it! The content script simply alters the background-color of the <body> element. Note that content scripts have limited access to the page’s DOM; they cannot access JavaScript variables of the page (unless the page explicitly exposes them), but they can modify the DOM freely. They also have access to some Chrome APIs, but not all. For example, they cannot use chrome.tabs directly—that’s why we use message passing.

One optional enhancement: you might want to save the user’s chosen color using chrome.storage so that it persists across browsing sessions. We’ll cover that in the Tips section. For now, the basic functionality is complete.

Step 4: Create the Background Service Worker (Optional but Recommended)

In manifest v3, background pages are replaced by service workers. They are event‑driven and run only when needed. For our simple extension, the background script isn’t strictly necessary because we can communicate directly between popup and content script. However, if you want to perform actions that don’t require a popup (e.g., respond to browser events like bookmark changes or page loads), a background worker is essential. Create background.js and leave it mostly empty for now, but let’s add a basic listener that logs when the extension is installed or updated:

chrome.runtime.onInstalled.addListener(() => {
  console.log('Extension installed or updated.');
});

This file is loaded as a service worker. You can use it to set up context menus, handle alarms, or manage global state using chrome.storage. Because service workers are non‑persistent, you cannot rely on global variables staying in memory. Always use storage APIs for persistence.

Step 5: Load and Test Your Extension in Chrome

Now comes the exciting part: seeing your creation come to life. Open Google Chrome and type chrome://extensions in the address bar. Enable “Developer mode” by toggling the switch at the top right. You’ll see three buttons: “Load unpacked”, “Pack extension”, and “Update”. Click “Load unpacked” and select your project folder. Chrome will load the extension, and you should see its icon appear in the toolbar (it might be a default puzzle piece if you didn’t provide proper icons). Click the icon to open the popup. Pick a color and click “Apply Color”. The background of the current webpage should change! Try navigating to a different page and clicking “Apply Color” again—it works because the content script runs on every page. Click “Reset to Default” to revert the color.

If nothing happens, open the developer tools for the extension (right‑click the extension icon → Inspect popup) and check the Console for errors. Common issues include: missing permissions, incorrect file paths in manifest.json, syntax errors in JSON or JavaScript, or the content script not matching the current tab’s URL. The chrome://extensions page also shows any errors. Use the “Service Worker” section to inspect the background script’s console. Debugging extensions is similar to debugging web pages; use console.log liberally. Once everything works, you have successfully built a functional browser extension from scratch.

Tips and Best Practices for Building Robust Browser Extensions

Tip 1: Use `chrome.storage` for Persistent Settings

Our current extension loses the color choice when the popup closes because we’re not saving it. To provide a better user experience, store the user’s color using chrome.storage.sync or chrome.storage.local. The sync storage syncs across all devices where the user is signed into Chrome. Modify popup.js to save the color when the apply button is clicked, and load it when the popup opens. In content.js, on page load, retrieve the saved color and apply it. This way, the user’s preference persists across tabs and browser sessions. Remember to add the "storage" permission to manifest.json (already done). Here’s a quick example:

// In popup.js - save color
chrome.storage.sync.set({ backgroundColor: color }, () => {
  console.log('Color saved');
});

// In popup.js - load stored color on open
chrome.storage.sync.get('backgroundColor', (data) => {
  if (data.backgroundColor) {
    document.getElementById('color').value = data.backgroundColor;
  }
});

// In content.js - apply stored color on page load
chrome.storage.sync.get('backgroundColor', (data) => {
  if (data.backgroundColor) {
    document.body.style.backgroundColor = data.backgroundColor;
  }
});

This simple addition dramatically improves usability. Always prefer storage over global variables for anything you want to survive.

Tip 2: Minimize Permissions and Follow the Principle of Least Privilege

When your extension requests permissions, users see a warning dialog. Excessive permissions can scare users away or lead to browser store rejection. Only ask for what you absolutely need. For example, instead of requesting "<all_urls>" in content scripts, use specific match patterns like "*://*.example.com/*" if your extension only works on certain sites. Use "activeTab" instead of "tabs" when you only need temporary access to the active tab. If you need to read or modify clipboard, ask for "clipboardRead" and "clipboardWrite" separately. Additionally, consider using optional permissions (optional_permissions in manifest) so that users can enable extra features later. The simpler your permission model, the higher the conversion rate.

Tip 3: Handle Extension Updates Gracefully

When you publish a new version, users will update automatically. However, your extension might need to migrate data or change behavior. Use the chrome.runtime.onInstalled listener in your background service worker to detect a version update. You can compare the previous version (stored in chrome.storage) with the current version and run migration code. Also, be aware that content scripts might still be running on pages; you may need to send a message to them to reload or update state. Another important aspect is the service worker lifecycle: when a service worker becomes inactive after a few seconds of no events, it stops. Do not rely on long‑running timers or global variables. Use chrome.alarms API for recurring tasks instead of setInterval.

FAQ: Frequently Asked Questions About Browser Extension Development

  1. Q: Can I build a browser extension without knowing JavaScript?
    A: Unfortunately, no. While there are some frameworks that generate extensions from config files, a deep understanding of JavaScript is essential for creating anything beyond a trivial extension. The core logic—manipulating the DOM, handling events, communicating between parts—is all JavaScript. You should be comfortable with modern ES6 syntax, promises, and asynchronous programming. However, you can learn the basics of JavaScript in a few weeks and then tackle extension development.
  2. Q: What is the difference between manifest v2 and v3?
    A: Manifest v3 is the current standard. The most significant change is the replacement of persistent background pages with service workers, which are non‑persistent and improve memory usage. Also, v3 restricts the use of eval() and requires that all code be bundled with the extension (no remote code execution). Remote hosted code is forbidden. Additionally, some APIs changed, e.g., browserAction is now under action. Chrome is gradually phasing out v2, so it’s crucial to develop in v3 from the start.
  3. Q: How do I debug content scripts?
    A: Content scripts run in the context of the web page. To debug them, open the developer tools of the page itself (F12) and look for a source named content.js or similar under the “Content Scripts” tab in the Sources panel. Alternatively, you can right‑click on the page and select “Inspect” then go to the Console. Any logs from console.log in your content script will appear there. You can also set breakpoints directly in the source.
  4. Q: Can I use npm packages or build tools like Webpack?
    A: Absolutely! Many developers use bundlers like Webpack, Rollup, or Parcel to manage dependencies, transpile TypeScript, and minify code. This is especially useful when your extension grows in complexity. You can treat your extension as a front‑end project with a build pipeline. Just ensure that the output files are correctly referenced in your manifest and that you do not include local modules that require a server. The final bundle is loaded unpacked or packed. Some popular boilplates exist for extension development with React or Vue.js.
  5. Q: How do I publish my extension on the Chrome Web Store?
    A: First, you need to create a developer account (one‑time fee of $5). Then, zip your extension folder (including all files) and upload the .zip file to the Chrome Web Store Developer Dashboard. Fill in the required details: description, screenshots, promo images, category, and privacy policy if you collect any data. The store will review your extension for policy compliance. This can take a few hours to a few days. After approval, your extension will be listed and available to millions of Chrome users. You can also distribute manually using the “Load unpacked” method, but that’s only for development.

Reference Tables for Quick Lookup

Table 1: Common Manifest v3 Fields and Their Purposes
Field Type Description
manifest_version integer Must be 3 for current Chrome extensions.
name string Display name of the extension (max 45 chars).
version string Version number (e.g., “1.0.0”).
permissions array List of API permissions (e.g., “storage”, “tabs”).
action object Defines toolbar button: popup, default icon, badge.
content_scripts array Scripts injected into web pages; requires match patterns.
background object Specifies the service worker file (non‑persistent).
icons object Required sizes: 16, 48, 128 (PNG or JPG).
Table 2: Essential Chrome Extension APIs for Beginners
API Used For Example
chrome.runtime Messaging, event listening, extension lifecycle chrome.runtime.sendMessage()
chrome.tabs Querying, creating, updating, and messaging tabs chrome.tabs.query({active:true})
chrome.storage Persistent data storage (sync or local) chrome.storage.sync.set()
chrome.action Setting toolbar icon, badge text, popup chrome.action.setBadgeText()
chrome.contextMenus Right‑click context menu items chrome.contextMenus.create()
chrome.alarms Scheduled background tasks chrome.alarms.create('myAlarm', {periodInMinutes: 1})
chrome.notifications Displaying desktop notifications chrome.notifications.create()

Conclusion: Your Journey from Prototype to Production

Building a browser extension is a rewarding experience that combines front‑end web development with a unique deployment model. In this tutorial, you learned how to set up the project structure with a manifest v3 file, create a popup interface, inject content scripts into web pages, and enable communication between different parts of the extension. You also gained insight into best practices like using persistent storage, minimizing permissions, and handling updates gracefully. The two reference tables should serve as quick guides for the most important manifest fields and APIs. Beyond the basic color changer, you can now expand your extension to include features like keyboard shortcuts (commands), option pages, or even integration with external APIs via fetch requests from the background service worker.

Remember that the browser extension ecosystem is huge. There are extensions with millions of users that started as simple experiments. The key to success is user experience: make your extension fast, lightweight, and intuitive. Always test on multiple sites, handle errors gracefully, and provide a way for users to configure settings. As you grow more confident, consider learning advanced topics like using Shadow DOM to isolate your styles from the host page, implementing declarative net request for ad blocking, or building a browser action that works offline. The skills you acquire here will also translate to building extensions for other browsers—Firefox uses a similar manifest format with minor differences (e.g., browser namespace instead of chrome, but with polyfills).

Finally, don’t hesitate to engage with the developer community. Sites like Stack Overflow, the Chromium Extensions mailing list, and GitHub repositories of popular extensions are goldmines of knowledge. If you encounter a bug or a missing feature, the Chrome DevTools allow you to inspect every part of your extension. And when you’re ready to share your creation with the world, the Chrome Web Store awaits. Good luck, and happy coding!

sarah antaboga
Author: sarah antaboga

Leave a Reply

Your email address will not be published. Required fields are marked *