General

How to Build a Chrome Extension in 2026 (Manifest V3 Complete Guide)

MatrixInn Solutions · Jun 04, 2026

Chrome extensions give you superpowers over the browser — blocking ads, automating tasks, injecting UI into web pages, capturing screenshots, managing tabs, and much more. The Chrome Web Store has over 100,000 extensions used by hundreds of millions of people.

This guide walks you through building a complete Chrome extension from scratch using Manifest V3 (MV3), the current extension platform required by Google since 2023.

What Changed in Manifest V3?

If you've followed older tutorials, be careful — most are outdated. Manifest V3 made several breaking changes from MV2:

  • Background pages → Service Workers. The old persistent background.html is gone. You now use a service worker that runs on demand and terminates when idle.
  • No remote code execution. You can no longer load JavaScript from external URLs. All code must be bundled in the extension package.
  • webRequest → declarativeNetRequest. The blocking webRequest API is deprecated. Ad blockers and request modifiers must use the declarativeNetRequest API instead.
  • Promises everywhere. Most chrome.* APIs now return Promises instead of requiring callbacks.

Extension Architecture

A Chrome extension is made of these components:

  • manifest.json — The configuration file. Declares permissions, scripts, and extension metadata.
  • Service Worker (background.js) — Runs in the background. Handles browser events like tab changes, alarms, and messages.
  • Content Scripts — JavaScript injected into web pages. Can read/modify DOM but can't access chrome.* APIs directly.
  • Popup — The HTML/JS/CSS UI shown when the user clicks the extension icon.
  • Options Page — A full settings page accessible via right-click on the icon.

Step 1 — Create manifest.json

Every extension starts with a manifest.json file:

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0",
  "description": "Does something awesome.",
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },
  "action": {
    "default_popup": "popup.html",
    "default_icon": "icons/icon48.png"
  },
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["https://*.example.com/*"],
      "js": ["content.js"]
    }
  ],
  "permissions": ["storage", "tabs", "activeTab"],
  "host_permissions": ["https://*.example.com/*"]
}

Key Fields Explained

  • manifest_version: 3 — Required. Tells Chrome this is an MV3 extension.
  • action — Defines the toolbar button and popup.
  • background.service_worker — Path to your background service worker file.
  • content_scripts — Scripts injected into matching pages. Use matches to control which URLs.
  • permissions — API permissions like storage, tabs, notifications.
  • host_permissions — Separate from permissions in MV3. Required to read/modify specific domains.

Step 2 — Build the Popup

Create popup.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <h1>My Extension</h1>
  <button id="btn-action">Do Something</button>
  <div id="status"></div>
  <script src="popup.js"></script>
</body>
</html>

Note: inline scripts (<script>...</script>) are blocked in extension HTML files by CSP. Always use external .js files.

Create popup.js:

document.getElementById('btn-action').addEventListener('click', async () => {
  // Query the active tab
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });

  // Send message to content script
  const response = await chrome.tabs.sendMessage(tab.id, { action: 'doSomething' });

  document.getElementById('status').textContent = response.result;
});

Step 3 — Write the Service Worker

Create background.js. This runs as a service worker — it will be terminated when idle and restarted on events:

// Listen for extension install
chrome.runtime.onInstalled.addListener(() => {
  console.log('Extension installed');
  chrome.storage.local.set({ enabled: true });
});

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

Critical MV3 service worker gotcha: Service workers terminate after ~30 seconds of inactivity. Don't store state in global variables — use chrome.storage.local for persistence.

Step 4 — Write the Content Script

Create content.js:

// Listen for messages from popup/background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'doSomething') {
    // Interact with the page DOM
    const title = document.title;
    sendResponse({ result: `Page title: ${title}` });
  }
  return true;
});

Content scripts can read and modify the DOM, but they run in an isolated world — they can't access JavaScript variables set by the page itself. Use chrome.runtime.sendMessage to communicate with the service worker.

Step 5 — Storage

Use chrome.storage.local (device only) or chrome.storage.sync (synced across user's Chrome instances):

// Save
await chrome.storage.local.set({ key: 'value' });

// Read
const data = await chrome.storage.local.get('key');
console.log(data.key); // 'value'

// Remove
await chrome.storage.local.remove('key');

Step 6 — Load the Extension Locally

  1. Open Chrome and go to chrome://extensions
  2. Enable Developer mode (toggle in top right)
  3. Click Load unpacked
  4. Select your extension folder
  5. Your extension appears in the toolbar

Every time you change code, click the reload button on the extensions page (or Ctrl+R on the extensions page).

Step 7 — Common Permissions Reference

  • "storage" — Read/write local or synced storage
  • "tabs" — Access tab URLs, titles, and metadata
  • "activeTab" — Temporary access to the active tab on user gesture (preferred over tabs for most use cases)
  • "notifications" — Show desktop notifications
  • "contextMenus" — Add items to the right-click context menu
  • "alarms" — Schedule periodic background tasks
  • "clipboardWrite" / "clipboardRead" — Clipboard access
  • "scripting" — Programmatically inject scripts with chrome.scripting.executeScript

Step 8 — Publish to the Chrome Web Store

  1. Create a Chrome Web Store developer account ($5 one-time fee)
  2. Zip your extension folder (not the folder itself, just its contents)
  3. Upload the .zip, add screenshots (1280×800 or 640×400), write a description
  4. Submit for review — typically takes 1–3 business days for new extensions

Common MV3 Mistakes to Avoid

  • Using eval() or innerHTML with external content — Blocked by CSP. Use textContent or sanitize carefully.
  • Storing state in service worker globals — The worker gets killed. Use chrome.storage instead.
  • Requesting too many permissions — Chrome Web Store reviewers flag this. Only request what you need.
  • Using inline scripts in HTML files — Not allowed. Always external .js files.
  • Forgetting return true in message listeners — Needed for async sendResponse, or the channel closes.

Next Steps

Once you have a working extension, consider these enhancements:

  • Add an options page for user settings
  • Use chrome.alarms for periodic background tasks
  • Implement context menus with chrome.contextMenus
  • Add keyboard shortcuts via the commands manifest key

Need a custom Chrome extension built for your business? See our extension development services — we build Manifest V3 extensions for enterprise and SaaS companies.

M
Written by
MatrixInn Solutions Engineering Team

We are a software house building mobile apps, SaaS products, AI automation, and browser extensions for clients in the US, UK, UAE, and worldwide. We publish what we learn from shipping real products — no filler, no fluff. About us →

Work with us

Got a project in mind?

We build mobile apps, SaaS products, and AI solutions. Let's talk.