Chrome extensions are one of the easiest ways to build a product that reaches millions of users. And with Manifest V3, Google has made the platform more secure and performant.
Here's everything you need to know to build your first Chrome extension.
Project Structure
A Chrome extension has 4 main parts:
my-extension/
├── manifest.json # Config file (the brain)
├── background/
│ └── background.js # Runs in background (API calls, events)
├── content/
│ ├── content.js # Injected into web pages
│ └── content.css # Styles for injected UI
├── popup/
│ ├── popup.html # Click extension icon → this opens
│ ├── popup.css
│ └── popup.js
└── icons/
├── icon16.png
├── icon48.png
└── icon128.png
Step 1: manifest.json
This is the config file that tells Chrome what your extension does:
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0.0",
"description": "What your extension does",
"permissions": ["storage", "activeTab"],
"action": {
"default_popup": "popup/popup.html",
"default_icon": { "48": "icons/icon48.png" }
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content/content.js"],
"css": ["content/content.css"]
}
],
"background": {
"service_worker": "background/background.js"
}
}
Key things:
-
manifest_version: 3— always use 3, version 2 is deprecated -
permissions— only request what you need (less permissions = faster review) -
content_scripts— runs on every page the user visits -
background— service worker, runs in the background
Step 2: Content Script — Inject UI into Pages
This is where the magic happens. Your content script runs on every webpage and can read/modify the DOM.
Here's a practical example — a floating tooltip that appears when the user selects text:
let tooltip = null;
document.addEventListener('mouseup', async (e) => {
if (e.target.closest('#my-tooltip')) return;
const selectedText = window.getSelection().toString().trim();
if (!selectedText || selectedText.length < 2) {
removeTooltip();
return;
}
showTooltip(e, selectedText);
});
function showTooltip(e, text) {
removeTooltip();
tooltip = document.createElement('div');
tooltip.id = 'my-tooltip';
tooltip.innerHTML = `
<div class="tooltip-header">
<span>MY EXTENSION</span>
<span class="close">×</span>
</div>
<div class="tooltip-content">${text}</div>
`;
document.body.appendChild(tooltip);
tooltip.style.position = 'absolute';
tooltip.style.top = `${e.pageY + 10}px`;
tooltip.style.left = `${e.pageX}px`;
tooltip.style.zIndex = '2147483647';
tooltip.querySelector('.close')
.addEventListener('click', removeTooltip);
}
function removeTooltip() {
if (tooltip) { tooltip.remove(); tooltip = null; }
}
Style it with content.css:
#my-tooltip {
background: #1a1a2e;
color: #e0e0e0;
border-radius: 10px;
padding: 10px 14px;
font-family: sans-serif;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
max-width: 350px;
}
Step 3: Background Service Worker
The background script handles things that don't need a visible page:
- API calls (avoids CORS issues)
- Context menus (right-click)
- Messages between content script and popup
- Notifications and alarms
// Run on install
chrome.runtime.onInstalled.addListener(() => {
chrome.storage.sync.set({ enabled: true });
chrome.contextMenus.create({
id: 'my-action',
title: 'Do something with "%s"',
contexts: ['selection']
});
});
// Handle messages from content script
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'apiCall') {
fetch(request.url)
.then(r => r.json())
.then(data => sendResponse({ success: true, data }))
.catch(err => sendResponse({ success: false }));
return true; // Keep channel open for async
}
});
Step 4: Popup
The popup opens when a user clicks your extension icon. It's just HTML:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<h1>My Extension</h1>
<label>
<input type="checkbox" id="toggle" checked>
Enable
</label>
<script src="popup.js"></script>
</body>
</html>
// popup.js
document.addEventListener('DOMContentLoaded', async () => {
const toggle = document.getElementById('toggle');
const { enabled } = await chrome.storage.sync.get({ enabled: true });
toggle.checked = enabled;
toggle.addEventListener('change', () => {
chrome.storage.sync.set({ enabled: toggle.checked });
});
});
Step 5: Message Passing
Content scripts can't make API calls directly (CORS). Send messages to the background script instead:
// content.js — send message
chrome.runtime.sendMessage(
{ action: 'apiCall', url: 'https://api.example.com/data' },
(response) => {
if (response.success) {
console.log(response.data);
}
}
);
// background.js — receive and handle
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'apiCall') {
fetch(request.url)
.then(r => r.json())
.then(data => sendResponse({ success: true, data }))
.catch(() => sendResponse({ success: false }));
return true;
}
});
Step 6: Storage
Chrome provides two storage types:
// sync — syncs across devices, small data (settings)
await chrome.storage.sync.set({ theme: 'dark', language: 'en' });
const settings = await chrome.storage.sync.get({ theme: 'dark' });
// local — local only, larger data (history, cache)
await chrome.storage.local.set({ history: [...items] });
const { history } = await chrome.storage.local.get({ history: [] });
Step 7: Load and Test
- Go to
chrome://extensions - Enable Developer mode (top right)
- Click Load unpacked
- Select your extension folder
- Visit any webpage and test
Every time you change code, click the reload button on the extensions page.
Step 8: Publish
- ZIP your extension folder
- Go to Chrome Web Store Developer Dashboard
- Pay $5 one-time registration fee
- Upload ZIP, add description and screenshots
- Submit for review (1-3 days)
Common Permissions Explained
| Permission | What it does |
|---|---|
activeTab |
Access current tab on user click |
storage |
Save/load settings and data |
contextMenus |
Add right-click menu items |
notifications |
Show desktop notifications |
tabs |
Access tab URLs and info |
Pro tip: Use fewer permissions = faster review + more user trust.
Quick Start Template
If you want to skip the setup and start building immediately, I put together a complete Chrome Extension Starter Kit with all of this already wired up — popup with tabs, content script with tooltip system, background worker, storage, dark theme, and options page.
Chrome Extension Starter Kit →
Questions? Drop a comment below. Happy building!
Top comments (0)