Published February 27, 2026 · 16 min read
Chrome extensions are one of the most accessible entry points into software development. With basic HTML, CSS, and JavaScript knowledge, you can build a functional extension in an afternoon. Chrome has over 3 billion users, so even a niche extension can reach a meaningful audience.
This guide walks you through every step: project structure, manifest.json configuration (Manifest V3), building a popup, writing content scripts, adding background service workers, and publishing to the Chrome Web Store. No prior extension experience required.
Every Chrome extension is a folder containing a few specific files. At minimum, you need a manifest.json file that tells Chrome what your extension does and what permissions it needs. From there, you can add a popup (the small window that appears when you click the extension icon), content scripts (JavaScript that runs on web pages), and background service workers (scripts that handle events and long-running logic).
| File | Purpose | Required |
|---|---|---|
manifest.json | Extension metadata, permissions, file references | Yes |
popup.html | UI shown when clicking the extension icon | No |
popup.js | JavaScript for popup interactivity | No |
content.js | Runs on web pages, can read/modify page DOM | No |
background.js | Service worker for events, alarms, messaging | No |
styles.css | Styles for popup or injected content | No |
icons/ | Extension icons (16px, 48px, 128px) | Recommended |
The manifest file is the heart of every Chrome extension. It declares the extension name, version, permissions, and which files to load. Chrome now requires Manifest V3 for all new extensions. Here is a complete starter manifest:
{
"manifest_version": 3,
"name": "My First Extension",
"version": "1.0.0",
"description": "A simple Chrome extension built from scratch.",
"permissions": ["activeTab", "storage"],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"background": {
"service_worker": "background.js"
}
}
manifest_version: 3 — Required. Chrome no longer accepts Manifest V2 for new submissions.
permissions — Declare what APIs your extension needs. activeTab gives access to the current tab when the user clicks your icon. storage lets you save data locally.
action — Defines the popup that appears when the user clicks the extension icon in the toolbar.
content_scripts — Scripts injected into web pages. The matches pattern controls which pages they run on.
background.service_worker — A service worker that runs in the background and responds to events.
The popup is a small HTML page that appears when users click your extension icon. It is a regular HTML file with CSS and JavaScript. The only difference from a normal web page is the size constraint (typically 300-400px wide) and the fact that inline scripts are blocked by Chrome's Content Security Policy.
Build a simple popup with a heading, a button, and a result area. Keep the design minimal. The popup should load instantly and provide a clear action for the user. Link your CSS and JS as external files (no inline scripts in Manifest V3).
<!DOCTYPE html>
<html>
<head>
<style>
body { width: 320px; padding: 16px; font-family: sans-serif; }
h1 { font-size: 18px; margin-bottom: 12px; }
button { background: #ff5f1f; color: #fff; border: none;
padding: 10px 20px; border-radius: 6px; cursor: pointer; }
#result { margin-top: 12px; color: #333; }
</style>
</head>
<body>
<h1>My Extension</h1>
<button id="btn">Count Words on Page</button>
<p id="result"></p>
<script src="popup.js"></script>
</body>
</html>
The popup script communicates with content scripts via Chrome's messaging API. When the user clicks the button, send a message to the content script running on the active tab and display the response.
document.getElementById('btn').addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({
active: true, currentWindow: true
});
const response = await chrome.tabs.sendMessage(
tab.id, { action: 'countWords' }
);
document.getElementById('result').textContent =
`Word count: ${response.count}`;
});
Content scripts run in the context of web pages. They can read and modify the DOM, extract data, inject UI elements, and communicate with the popup and background scripts. They run in an isolated world, meaning they share the page DOM but not the page's JavaScript variables.
This content script listens for messages from the popup and responds with the word count of the current page's visible text.
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'countWords') {
const text = document.body.innerText || '';
const count = text.split(/\s+/).filter(w => w.length > 0).length;
sendResponse({ count });
}
});
Content scripts can access the DOM but cannot use most Chrome APIs directly. They communicate with background scripts and popups via chrome.runtime.sendMessage() and chrome.runtime.onMessage. For sensitive operations (storage, network requests, alarms), route messages through the background service worker.
In Manifest V3, background pages are replaced by service workers. Service workers are event-driven: they start when an event occurs and terminate when idle. They do not have access to the DOM but can use all Chrome extension APIs.
A background service worker that runs when the extension is installed, listens for tab updates, and manages extension state.
chrome.runtime.onInstalled.addListener(() => {
console.log('Extension installed');
chrome.storage.local.set({ totalCounts: 0 });
});
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'saveCount') {
chrome.storage.local.get(['totalCounts'], (result) => {
const updated = (result.totalCounts || 0) + request.count;
chrome.storage.local.set({ totalCounts: updated });
sendResponse({ totalCounts: updated });
});
return true; // Keep message channel open for async response
}
});
Service workers in Manifest V3 do not run persistently. They wake up for events and go back to sleep. This means you cannot rely on global variables staying in memory. Use chrome.storage for any data that needs to persist between service worker activations. This is the biggest change from Manifest V2 background pages.
Open Chrome and navigate to chrome://extensions/. Enable "Developer mode" in the top right. Click "Load unpacked" and select your extension folder. Your extension icon should appear in the toolbar immediately.
Right-click the extension icon and select "Inspect popup" to open DevTools for the popup. You can see console logs, set breakpoints, and debug JavaScript just like a normal web page.
Open DevTools on any web page (F12), go to the Sources tab, and find your content script under the "Content scripts" section. You can set breakpoints and step through the code.
On the chrome://extensions/ page, find your extension and click "Inspect views: service worker." This opens DevTools for the background service worker where you can see logs and debug events.
Once your extension works correctly in developer mode, you can publish it to the Chrome Web Store for anyone to install.
| Step | Details | Cost |
|---|---|---|
| Create Developer Account | Sign up at the Chrome Web Store Developer Dashboard | $5 one-time fee |
| Prepare Store Assets | Icon (128px), screenshots (1280x800), description, category | Free |
| Package Extension | Zip your extension folder (exclude .git and node_modules) | Free |
| Upload and Submit | Upload zip, fill in listing details, submit for review | Free |
| Review Process | Google reviews for policy compliance. Takes 1-7 business days | Free |
Before submitting: (1) Remove all console.log statements. (2) Request only the minimum permissions your extension actually uses. (3) Write a clear, honest description. (4) Include at least two screenshots showing the extension in action. (5) Set up a support email or website. (6) Test on Chrome Stable, Beta, and Dev channels if possible.
If you find older tutorials online, they likely use Manifest V2. Here are the key differences to be aware of.
| Feature | Manifest V2 | Manifest V3 |
|---|---|---|
| Background | Persistent background pages | Event-driven service workers |
| Content Security Policy | Relaxed (inline scripts allowed) | Strict (no inline scripts) |
| Network Requests | webRequest (blocking) | declarativeNetRequest (rules-based) |
| Remote Code | Allowed (eval, remote scripts) | Not allowed |
| Permissions | Broad permissions on install | Granular, runtime permissions encouraged |
| Store Acceptance | No longer accepted for new submissions | Required for all new extensions |
Use our free Code Formatter to clean up your HTML, CSS, and JavaScript before publishing your extension.
Get the Free Code FormatterBasic JavaScript is required for anything interactive. If your extension is purely a popup with static information, you only need HTML and CSS. But most useful extensions involve content scripts or background logic, which require JavaScript. You do not need to be an expert. If you can write event listeners, manipulate the DOM, and handle async functions, you have enough JavaScript knowledge to build a solid extension.
New submissions typically take 1 to 7 business days for review. Updates to existing extensions are usually faster, often within 1-3 days. Extensions requesting sensitive permissions (like access to all URLs or browsing history) may take longer due to additional scrutiny. Keep your permissions minimal to speed up the review process.
Yes. Common monetization strategies include: freemium models (basic features free, premium features paid), one-time purchase via the Chrome Web Store payments API, subscription via your own payment system (Stripe, Gumroad), and affiliate links within the extension popup. The Chrome Web Store no longer supports its built-in payments, so most developers use external payment providers like Stripe or LemonSqueezy.