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
- Go to
chrome://extensions. - Turn on Developer mode (top right).
- Click Load unpacked and choose your folder.
- 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
declarativeNetRequestrules 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
- Register as a Chrome Web Store developer (a one-time fee).
- Zip the extension folder.
- Upload it in the Developer Dashboard with a description, screenshots, icon and a privacy disclosure of what data you collect.
- 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
What Is TypeScript? JavaScript With Labels, Explained for Beginners
TypeScript is JavaScript plus types: labels that say what kind of value each thing holds, checked before your code runs. What it looks like, why AI tools default to it, what .ts and .tsx files are, and how to read the red squiggles.
What Is a Wireframe? (And Why Sketch Before You Build With AI)
A wireframe is a simple sketch of a screen's layout — boxes, labels, buttons — without colours or styling. What wireframes are for, wireframe vs mockup vs prototype, tools from paper to Figma, and how a quick wireframe makes AI app builders produce what you actually want.