How to Create a Chrome Extension in 2026 - Step by Step Guide

What Is a Chrome Extension

A Chrome extension is a small software program that customizes the Chrome browsing experience. Extensions can modify web pages, add UI elements to the browser, intercept network requests, manage tabs, and interact with Chrome's built-in features. They are built with standard web technologies: HTML, CSS, and JavaScript.

As of 2026, all new Chrome extensions must use Manifest V3 (MV3), the latest extension platform. MV3 introduced significant architectural changes from the previous MV2 format, including the replacement of persistent background pages with event-driven service workers, the shift from the webRequest API to the declarativeNetRequest API for network modification, and stricter content security policies. These changes improve security, performance, and privacy at the cost of some flexibility that MV2 developers had enjoyed.

This guide walks through building a complete Chrome extension from scratch using MV3. By the end, you will have a working extension that you can load into Chrome for testing and publish to the Chrome Web Store.

Prerequisites

To follow this guide, you need:

No build tools, bundlers, or frameworks are required for a basic extension. You can build everything with plain files. For larger projects, you may choose to add a bundler like Vite or Webpack, but that is optional and not covered in this introductory guide.

If you need to validate your JSON manifest file, our JSON Formatter tool checks syntax and formats JSON with proper indentation.

Project Structure

A Chrome extension is a directory containing specific files that Chrome knows how to read. The minimum required file is manifest.json. A typical extension project looks like this:

my-extension/
  manifest.json
  service-worker.js
  popup/
    popup.html
    popup.css
    popup.js
  content/
    content.js
    content.css
  icons/
    icon-16.png
    icon-32.png
    icon-48.png
    icon-128.png

Each file serves a specific purpose. The manifest declares the extension's metadata and capabilities. The service worker runs in the background and handles events. Content scripts run in the context of web pages. The popup provides a UI when the user clicks the extension icon. Icons are required at multiple sizes for display in the toolbar, extensions page, and Chrome Web Store.

The Manifest File (manifest.json)

The manifest is the most important file in your extension. It tells Chrome everything about the extension: its name, version, permissions, and which scripts to load. Here is a complete manifest for a typical extension:

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0.0",
  "description": "A brief description of what the extension does.",
  "icons": {
    "16": "icons/icon-16.png",
    "32": "icons/icon-32.png",
    "48": "icons/icon-48.png",
    "128": "icons/icon-128.png"
  },
  "action": {
    "default_popup": "popup/popup.html",
    "default_icon": {
      "16": "icons/icon-16.png",
      "32": "icons/icon-32.png"
    },
    "default_title": "Click to open"
  },
  "background": {
    "service_worker": "service-worker.js"
  },
  "content_scripts": [
    {
      "matches": ["https://*.example.com/*"],
      "js": ["content/content.js"],
      "css": ["content/content.css"],
      "run_at": "document_idle"
    }
  ],
  "permissions": ["storage", "activeTab"],
  "host_permissions": ["https://*.example.com/*"]
}

Let us walk through the key fields:

For viewing and editing JSON structures interactively, our JSON Viewer tool provides a tree view with collapsible nodes.

Background Service Worker

The service worker is the central event handler for your extension. Unlike MV2 background pages that ran persistently, MV3 service workers start when an event fires and stop when idle. This means you cannot rely on global variables persisting between events. Use the chrome.storage API to persist state.

Here is a basic service worker that responds to the extension being installed and handles messages from other parts of the extension:

// service-worker.js

// Runs when the extension is first installed or updated
chrome.runtime.onInstalled.addListener((details) => {
  if (details.reason === 'install') {
    // Set default settings on first install
    chrome.storage.local.set({
      enabled: true,
      theme: 'dark',
      count: 0
    });
  }
});

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

  if (message.type === 'INCREMENT') {
    chrome.storage.local.get('count', (data) => {
      const newCount = (data.count || 0) + 1;
      chrome.storage.local.set({ count: newCount }, () => {
        sendResponse({ count: newCount });
      });
    });
    return true;
  }
});

// React to tab updates
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  if (changeInfo.status === 'complete' && tab.url) {
    // Tab finished loading, perform any needed action
    console.log('Tab loaded:', tab.url);
  }
});

Important patterns to note in service worker development:

Content Scripts

Content scripts run in the context of web pages that match the URL patterns defined in your manifest. They can read and modify the DOM of the page but run in an isolated world, meaning they cannot access JavaScript variables or functions defined by the page itself (and vice versa).

// content/content.js

// This script runs on pages matching the manifest's content_scripts pattern

function highlightLinks() {
  const links = document.querySelectorAll('a[href]');
  links.forEach(link => {
    // Add a custom data attribute to track processed links
    if (!link.dataset.extensionProcessed) {
      link.dataset.extensionProcessed = 'true';
      link.style.outline = '2px solid #00ff88';
    }
  });
}

// Run when the DOM is ready
highlightLinks();

// Watch for dynamically added content
const observer = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    if (mutation.addedNodes.length > 0) {
      highlightLinks();
    }
  }
});

observer.observe(document.body, {
  childList: true,
  subtree: true
});

// Listen for messages from the popup or service worker
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'TOGGLE_HIGHLIGHTS') {
    const links = document.querySelectorAll('a[data-extension-processed]');
    links.forEach(link => {
      link.style.outline = message.enabled ? '2px solid #00ff88' : '';
    });
    sendResponse({ success: true });
  }
});

Content scripts have access to a limited set of Chrome APIs: runtime (for messaging), storage, and i18n. For anything else, the content script must send a message to the service worker, which has full API access.

The run_at property in the manifest controls when the content script executes. Options are document_start (before the DOM is constructed), document_end (after the DOM is complete but before images and subframes load), and document_idle (after the page finishes loading, the default).

The popup is a small HTML page that appears when the user clicks your extension's toolbar icon. It has its own execution context, separate from both the page and the service worker. The popup closes whenever the user clicks outside it.

<!-- popup/popup.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <div class="popup-container">
    <h1>My Extension</h1>
    <p>Links found: <span id="count">0</span></p>
    <label class="toggle">
      <input type="checkbox" id="enabled" checked>
      <span>Enable highlighting</span>
    </label>
    <button id="refresh">Refresh Count</button>
  </div>
  <script src="popup.js"></script>
</body>
</html>
/* popup/popup.css */
body {
  width: 280px;
  padding: 16px;
  font-family: 'Inter', sans-serif;
  background: #0a0a0f;
  color: #e0e0e8;
  margin: 0;
}

h1 {
  font-size: 1.2rem;
  color: #fff;
  margin: 0 0 12px;
}

.toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 12px 0;
  cursor: pointer;
}

button {
  width: 100%;
  padding: 10px;
  background: #00ff88;
  color: #0a0a0f;
  border: none;
  border-radius: 6px;
  font-weight: 600;
  cursor: pointer;
  margin-top: 8px;
}

button:hover {
  background: #00cc6e;
}
// popup/popup.js

document.addEventListener('DOMContentLoaded', () => {
  const countEl = document.getElementById('count');
  const enabledEl = document.getElementById('enabled');
  const refreshBtn = document.getElementById('refresh');

  // Load saved state
  chrome.storage.local.get(['enabled', 'count'], (data) => {
    enabledEl.checked = data.enabled !== false;
    countEl.textContent = data.count || 0;
  });

  // Toggle highlighting
  enabledEl.addEventListener('change', () => {
    const enabled = enabledEl.checked;
    chrome.storage.local.set({ enabled });

    // Send message to active tab's content script
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
      if (tabs[0]) {
        chrome.tabs.sendMessage(tabs[0].id, {
          type: 'TOGGLE_HIGHLIGHTS',
          enabled
        });
      }
    });
  });

  // Refresh count from service worker
  refreshBtn.addEventListener('click', () => {
    chrome.runtime.sendMessage({ type: 'GET_COUNT' }, (response) => {
      if (response) {
        countEl.textContent = response.count;
      }
    });
  });
});

Note that inline JavaScript is not allowed in extension HTML files due to MV3's Content Security Policy. All JavaScript must be in separate .js files loaded via <script src="..."> tags. Inline event handlers like onclick="" are also prohibited. Use addEventListener instead.

Understanding Permissions

Permissions are central to how Chrome extensions work. Every API or capability your extension uses must be declared in the manifest. There are three categories:

API permissions (permissions array): these grant access to specific Chrome APIs. Common ones include:

Host permissions (host_permissions array): URL patterns that define which websites the extension can interact with. For example, "https://*.google.com/*" grants access to all Google subdomains. Use "<all_urls>" only when you genuinely need access to every website, as it triggers a strong permission warning and reduces install rates.

Optional permissions (optional_permissions array): permissions that are not granted at install time but can be requested at runtime using chrome.permissions.request(). This pattern is preferable for features that not every user needs, as it keeps the initial permission warning minimal.

Tip: Use activeTab instead of broad host permissions whenever possible. It grants temporary access to the current tab only when the user actively engages with your extension (by clicking the icon), and it does not trigger a permission warning dialog. This is the most user-friendly permission model.

Storage and State Management

MV3 service workers are ephemeral, so persistent storage is critical. Chrome provides several storage mechanisms:

chrome.storage.local stores data locally with a default limit of 10 MB (increase to unlimited with the unlimitedStorage permission). Data persists until the extension is uninstalled. This is the primary storage mechanism for most extensions.

chrome.storage.sync syncs data across the user's Chrome instances. Limited to 100 KB total and 8 KB per item. Use this for user preferences that should follow them across devices.

chrome.storage.session stores data only for the current browser session. Data is cleared when Chrome closes. This is useful for temporary state that the service worker needs to access across event handlers but should not persist long-term.

// Writing to storage
chrome.storage.local.set({
  settings: { theme: 'dark', fontSize: 14 },
  lastUpdated: Date.now()
});

// Reading from storage
chrome.storage.local.get(['settings', 'lastUpdated'], (result) => {
  console.log(result.settings); // { theme: 'dark', fontSize: 14 }
  console.log(result.lastUpdated); // timestamp
});

// Listening for changes
chrome.storage.onChanged.addListener((changes, areaName) => {
  if (areaName === 'local' && changes.settings) {
    console.log('Old settings:', changes.settings.oldValue);
    console.log('New settings:', changes.settings.newValue);
  }
});

Messaging Between Components

Chrome extensions have multiple execution contexts (service worker, popup, content scripts, options page) that need to communicate. The messaging system provides this communication channel.

One-time messages use chrome.runtime.sendMessage() to send and chrome.runtime.onMessage.addListener() to receive. For content scripts, use chrome.tabs.sendMessage(tabId, message) to send messages from the service worker or popup to a specific tab.

// From popup to service worker
chrome.runtime.sendMessage(
  { type: 'SAVE_DATA', payload: { key: 'value' } },
  (response) => {
    console.log('Service worker responded:', response);
  }
);

// From service worker to content script in a specific tab
chrome.tabs.sendMessage(tabId,
  { type: 'UPDATE_UI', data: { color: '#00ff88' } },
  (response) => {
    console.log('Content script responded:', response);
  }
);

For long-lived connections (streaming data, for example), use chrome.runtime.connect() to establish a port-based connection. Ports remain open until explicitly disconnected and support bidirectional messaging.

Loading and Testing Your Extension

To test your extension during development:

  1. Open Chrome and navigate to chrome://extensions/.
  2. Enable "Developer mode" using the toggle in the top-right corner.
  3. Click "Load unpacked" and select your extension's directory.
  4. Your extension appears in the list with its icon in the toolbar (you may need to pin it from the puzzle piece menu).

After making changes to your code, click the refresh icon on your extension's card in chrome://extensions/ to reload it. Changes to the service worker and content scripts require this reload. Changes to the popup HTML/CSS/JS take effect the next time you open the popup.

For debugging each component:

The chrome://extensions/ page also shows any errors encountered by your extension. A red error badge appears on the extension card when errors are logged. Click "Errors" to see the details.

Publishing to the Chrome Web Store

When your extension is ready for public release, follow these steps to publish it:

  1. Register as a Chrome Web Store developer at the Chrome Web Store Developer Console. The one-time registration fee is $5 USD.
  2. Create a ZIP file of your extension directory. The ZIP should contain the files directly (not a parent folder).
  3. In the Developer Console, click "New Item" and upload the ZIP file.
  4. Fill in the store listing details: detailed description, at least one screenshot (1280x800 or 640x400), category, and language.
  5. If your extension uses certain permissions (like activeTab, tabs, or host permissions), you need to provide a justification for each in the "Privacy" tab. Google reviews these justifications during the review process.
  6. Submit for review. New extensions typically take 1-3 business days to be reviewed. Updates to existing extensions are usually faster.

Before submission, make sure your extension meets the Chrome Web Store policies. Common rejection reasons include: requesting unnecessary permissions, lacking a privacy policy (required if you handle user data), misleading descriptions, and quality issues like broken functionality.

For generating proper JSON-LD structured data for your extension's landing page, our Schema Generator creates valid schema.org markup. Before deployment, use the JS Minifier to reduce your extension's file size.

Frequently Asked Questions

Yes. Chrome extensions are built with HTML, CSS, and JavaScript. You need a working knowledge of JavaScript to handle extension logic, event listeners, Chrome API calls, and DOM manipulation in content scripts. Familiarity with asynchronous JavaScript (Promises and async/await) is particularly important since most Chrome extension APIs are asynchronous.

Manifest V3 (MV3) replaces persistent background pages with service workers that run only when needed, replaces the webRequest blocking API with the declarativeNetRequest API for network modification, requires host permissions to be declared separately, and enforces stricter content security policies. MV3 is now required for all new Chrome Web Store submissions, and existing MV2 extensions must migrate.

Publishing to the Chrome Web Store requires a one-time developer registration fee of $5 USD. There are no ongoing fees for hosting or distribution. Google reviews each submission, which typically takes 1-3 business days for new extensions and a few hours for updates to existing ones.

Only if the user grants permission. In Manifest V3, host permissions are requested at install time or at runtime. Users can choose to grant access to all sites, specific sites, or only on click. Extensions that request fewer permissions generally have higher install rates because users are more comfortable granting limited access.

For the popup UI, right-click the extension icon and select "Inspect Popup" to open DevTools for the popup context. For the service worker, go to chrome://extensions, find your extension, and click "Service Worker" to open its DevTools console. For content scripts, use the regular page DevTools and look for your script files in the Sources panel under the Content Scripts section.

Wikipedia

A browser extension is a small software module for customizing a web browser, enabling users to tailor browser functionality and behavior to individual needs.

Source: Wikipedia - Browser Extension · Verified March 20, 2026

Stack Overflow Community

743How to create a Chrome extension287Chrome extension manifest v3 guide456Content scripts in Chrome extensions

Video Tutorials

▶ Watch related tutorials on YouTube

Quick Facts

Manifest
V3
Languages
JS/HTML/CSS
Store Fee
$5
Review
1-3 days

Update History

March 20, 2026 - Article published with comprehensive coverage
March 19, 2026 - Research and drafting completed

Browser Compatibility

Chrome 90+ Firefox 88+ Safari 14+ Edge 90+ Opera 76+