Blog
4 min read

How to Make a Chrome Extension (Manifest V3, Step by Step)

Build a working Chrome extension from scratch: manifest.json, a popup, content scripts that change web pages, the background service worker, permissions, storage, loading it unpacked for testing, and publishing to the Chrome Web Store — plus what Manifest V3 changed.

A Chrome extension is a small bundle of HTML, CSS and JavaScript that adds features to the browser — a toolbar button, changes to web pages, shortcuts, side panels. The same extension usually works in Edge, Brave and other Chromium browsers too. (HTML, CSS, and JavaScript explained)

Chrome extensions now use Manifest V3, the current format.

The parts of an extension

Part What it does
manifest.json Describes the extension: name, version, permissions, which files do what
Popup The small window when you click the toolbar icon
Content script JavaScript injected into web pages — can read and change them
Background service worker Runs in the background, handles events (no page of its own)
Options page / side panel Optional extra UI

Step 1: the manifest

Make a folder my-extension/ with manifest.json:

{
  "manifest_version": 3,
  "name": "Word Counter",
  "version": "1.0.0",
  "description": "Counts the words on the current page.",
  "action": {
    "default_popup": "popup.html",
    "default_icon": "icon128.png"
  },
  "icons": { "128": "icon128.png" },
  "permissions": ["activeTab", "scripting", "storage"]
}

Step 2: a popup

popup.html:

<!doctype html>
<html>
  <body style="width: 220px; font-family: sans-serif">
    <button id="count">Count words</button>
    <p id="result"></p>
    <script src="popup.js"></script>
  </body>
</html>

Extensions can't run inline <script> code — scripts must be separate files.

popup.js:

document.getElementById('count').addEventListener('click', async () => {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
  const [{ result }] = await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => document.body.innerText.trim().split(/\s+/).length,
  })
  document.getElementById('result').textContent = `${result} words`
})

activeTab + scripting lets the popup run code on the current page when the user clicks — without asking for access to every website.

Step 3: load it in Chrome

  1. Go to chrome://extensions.
  2. Turn on Developer mode (top right).
  3. Click Load unpacked and choose your folder.
  4. Pin the extension and click it.

After changing files, click the reload icon on the extension's card. Inspect the popup by right-clicking it → Inspect. (Browser developer tools)

Content scripts: change pages automatically

To run on certain sites every time, declare a content script:

"content_scripts": [{
  "matches": ["https://*.example.com/*"],
  "js": ["content.js"]
}]

content.js runs inside matching pages and can read or modify the DOM — hide elements, add buttons, highlight text.

The background service worker

"background": { "service_worker": "background.js" }

It handles events — the extension being installed, messages from content scripts, alarms, context-menu clicks. In Manifest V3 it's a service worker: it stops when idle and restarts on events, so don't keep important state in variables — use chrome.storage:

await chrome.storage.local.set({ count: 5 })
const { count } = await chrome.storage.local.get('count')

What Manifest V3 changed

  • Service workers replaced always-running background pages.
  • No remotely hosted code — all JavaScript must ship inside the extension. You can still call APIs with fetch.
  • Network request blocking moved to the declarativeNetRequest rules API (this is what changed ad blockers).
  • Stricter content security rules.

Old tutorials using manifest_version: 2, background.page or chrome.browserAction are outdated.

Permissions: ask for as little as possible

Every permission appears in the install prompt and the Web Store review. Broad ones like "<all_urls>" scare users and slow review. Prefer activeTab, specific host patterns, and optional permissions requested when needed. (Principle of least privilege)

Publishing to the Chrome Web Store

  1. Register as a Chrome Web Store developer (a one-time fee).
  2. Zip the extension folder.
  3. Upload it in the Developer Dashboard with a description, screenshots, icon and a privacy disclosure of what data you collect.
  4. Wait for review — from hours to days.

API keys in extensions

Anything in an extension can be read by anyone who installs it. Don't put secret API keys in it — call your own back end, which holds the key. (Keep API keys out of an AI-built app)

Building it with AI

Extensions are a great project for AI coding tools — small, self-contained, quick to test. Tell the AI to use Manifest V3 explicitly, so it doesn't produce outdated V2 code. (How to prompt AI coding tools)


EasySpawn runs the back end your extension calls — keeping API keys on the server, with accounts and a Postgres database — so the extension itself holds no secrets. See how it works or join the waitlist.

Related: HTML, CSS, and JavaScript Explained · How to Keep API Keys Out of an AI-Built App · Browser Developer Tools for Beginners · What Is an API?

Keep reading