Chrome Extensions 101: The Ultimate Guide
Your browser is more powerful than you think. Chrome Extensions let you bend it to your will.
Chrome Extensions are one of the most underrated superpowers in web development. With a few files and some JavaScript, you can modify any website, automate workflows, block content, add features, and even build full-blown SaaS products that live in the browser.
This guide covers everything — from your first "Hello World" extension to publishing on the Chrome Web Store.
Table of Contents
- What Are Chrome Extensions?
- The Architecture — How Extensions Work
- Manifest.json — The Blueprint
- Core Components Deep Dive
- Chrome APIs — Your Toolbox
- Building Your First Extension
- Real-World Extension Patterns
- Storage & State Management
- Messaging — Making Components Talk
- Permissions — The Trust System
- Debugging Extensions
- Publishing to Chrome Web Store
- Manifest V3 vs V2
- Monetization Strategies
- Common Pitfalls
What Are Chrome Extensions?
Chrome Extensions are small programs built with HTML, CSS, and JavaScript that customize the browser experience. They run in a sandboxed environment with access to special Chrome APIs that regular web pages can't touch.
What can they do?
- Modify the content of any webpage (ad blockers, dark mode)
- Add UI elements to the browser (toolbar popups, side panels)
- Intercept and modify network requests
- Manage tabs, bookmarks, and history
- Interact with the clipboard, notifications, and storage
- Run background tasks and scheduled jobs
Big names built as extensions: Grammarly, LastPass, React DevTools, Honey, uBlock Origin, Momentum.
The Architecture
Every Chrome Extension has a specific architecture. Understanding it is the key to building anything meaningful.
┌─────────────────────────────────────────────────────────┐
│ CHROME BROWSER │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ │
│ │ Popup UI │ │ Side Panel │ │ Options Page │ │
│ │ (popup.html)│ │(sidepanel.html)│ │ (options.html) │ │
│ └──────┬──────┘ └──────┬───────┘ └───────┬────────┘ │
│ │ │ │ │
│ └────────────────┼───────────────────┘ │
│ │ Chrome Messaging API │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Service Worker │ │
│ │ (background.js) │ │
│ │ • Event handling │ │
│ │ • API calls │ │
│ │ • State management │ │
│ └───────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Content Scripts │ │
│ │ (Injected into web pages you visit) │ │
│ │ • Read/modify DOM │ │
│ │ • Listen to page events │ │
│ │ • Communicate with background │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
The Key Players
| Component | What It Does | Runs When |
|---|---|---|
| Manifest.json | Config file — declares permissions, scripts, UI | Always (it's the entry point) |
| Service Worker | Background logic, event handling, API calls | On events (not always running) |
| Content Scripts | Injected into web pages, can modify DOM | When you visit matching pages |
| Popup | Small UI that opens when you click the extension icon | On icon click |
| Options Page | Settings page for the extension | When user opens settings |
| Side Panel | Persistent panel on the side of the browser | When toggled open |
Manifest.json — The Blueprint
Every extension starts with manifest.json. It tells Chrome everything about your extension.
{
"manifest_version": 3,
"name": "My Awesome Extension",
"version": "1.0.0",
"description": "Does something awesome",
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"action": {
"default_popup": "popup.html",
"default_icon": "icons/icon48.png",
"default_title": "Click me!"
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["https://*.google.com/*"],
"js": ["content.js"],
"css": ["content.css"]
}
],
"permissions": ["storage", "activeTab", "notifications"],
"host_permissions": ["https://*.example.com/*"],
"options_page": "options.html"
}
Key Fields Explained
| Field | Purpose |
|---|---|
manifest_version |
Must be 3 (V2 is deprecated) |
action |
Toolbar icon behavior — popup, icon, tooltip |
background.service_worker |
Background script (replaces V2's persistent background page) |
content_scripts |
Scripts injected into matching web pages |
permissions |
What Chrome APIs you need access to |
host_permissions |
Which websites your extension can interact with |
Core Components Deep Dive
1. Service Worker (Background Script)
The brain of your extension. It listens for events, manages state, and coordinates between components. In MV3, it's a service worker — meaning it's not always running. It wakes up on events and goes idle when done.
// background.js
// Runs when extension is installed or updated
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
console.log('Extension installed! Setting defaults...');
chrome.storage.local.set({ enabled: true, theme: 'dark' });
}
});
// Listen for messages from content scripts or popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'FETCH_DATA') {
fetch('https://api.example.com/data')
.then((res) => res.json())
.then((data) => sendResponse({ success: true, data }))
.catch((err) => sendResponse({ success: false, error: err.message }));
return true; // Keep the message channel open for async response
}
});
// React to tab changes
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url?.includes('github.com')) {
chrome.tabs.sendMessage(tabId, { type: 'GITHUB_PAGE_LOADED' });
}
});
// Context menu (right-click menu)
chrome.contextMenus.create({
id: 'lookupSelection',
title: 'Look up "%s"',
contexts: ['selection'],
});
chrome.contextMenus.onClicked.addListener((info) => {
if (info.menuItemId === 'lookupSelection') {
console.log('User selected:', info.selectionText);
}
});
2. Content Scripts
These run inside web pages. They can read and modify the DOM, but they live in an isolated world — they can't access the page's JavaScript variables directly.
// content.js — Injected into web pages
// Modify the page
document.querySelectorAll('img').forEach((img) => {
img.style.filter = 'grayscale(100%)'; // Make all images grayscale
});
// Add a floating widget
const widget = document.createElement('div');
widget.id = 'my-extension-widget';
widget.innerHTML = `
<div style="position:fixed; bottom:20px; right:20px;
background:#1a1a2e; color:white; padding:16px;
border-radius:12px; z-index:99999; font-family:system-ui;">
<h3>My Extension</h3>
<p>Reading time: ${estimateReadingTime()} min</p>
<button id="ext-save-btn">Save Article</button>
</div>
`;
document.body.appendChild(widget);
// Listen for messages from background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'EXTRACT_TEXT') {
const text = document.body.innerText;
sendResponse({ text });
}
});
function estimateReadingTime() {
const words = document.body.innerText.split(/\s+/).length;
return Math.ceil(words / 200);
}
3. Popup UI
The popup appears when users click your extension icon. It's a regular HTML page with its own JS and CSS.
<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
<style>
body { width: 320px; padding: 16px; font-family: system-ui; }
.toggle { display: flex; align-items: center; gap: 8px; }
.switch { position: relative; width: 48px; height: 24px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; inset: 0; background: #ccc;
border-radius: 24px; cursor: pointer; transition: 0.3s; }
.slider::before { content: ""; position: absolute; height: 18px;
width: 18px; left: 3px; bottom: 3px; background: white;
border-radius: 50%; transition: 0.3s; }
input:checked + .slider { background: #4CAF50; }
input:checked + .slider::before { transform: translateX(24px); }
.stats { margin-top: 12px; padding: 12px; background: #f5f5f5;
border-radius: 8px; }
</style>
</head>
<body>
<h2>My Extension</h2>
<div class="toggle">
<label class="switch">
<input type="checkbox" id="enableToggle">
<span class="slider"></span>
</label>
<span>Enabled</span>
</div>
<div class="stats">
<p>Pages modified: <strong id="pageCount">0</strong></p>
</div>
<script src="popup.js"></script>
</body>
</html>
// popup.js
const toggle = document.getElementById('enableToggle');
const pageCount = document.getElementById('pageCount');
// Load saved state
chrome.storage.local.get(['enabled', 'count'], (data) => {
toggle.checked = data.enabled ?? true;
pageCount.textContent = data.count ?? 0;
});
// Save state on toggle
toggle.addEventListener('change', () => {
chrome.storage.local.set({ enabled: toggle.checked });
// Notify content scripts
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
chrome.tabs.sendMessage(tabs[0].id, {
type: 'TOGGLE',
enabled: toggle.checked,
});
});
});
Chrome APIs — Your Toolbox
Here are the most useful Chrome APIs:
Storage API
// Save data (persistent across sessions)
chrome.storage.local.set({ key: 'value', user: { name: 'Ishaan' } });
// Read data
chrome.storage.local.get(['key', 'user'], (result) => {
console.log(result.key); // 'value'
console.log(result.user.name); // 'Ishaan'
});
// Sync storage (syncs across devices if user is signed in)
chrome.storage.sync.set({ theme: 'dark' });
// Listen for changes
chrome.storage.onChanged.addListener((changes, area) => {
if (changes.theme) {
console.log('Theme changed:', changes.theme.oldValue, '→', changes.theme.newValue);
}
});
Tabs API
// Get the current active tab
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
console.log(tabs[0].url);
});
// Create a new tab
chrome.tabs.create({ url: 'https://example.com' });
// Inject a script into a tab
chrome.scripting.executeScript({
target: { tabId: tabId },
func: () => document.title,
}).then((results) => console.log(results[0].result));
Alarms API (Scheduled Tasks)
// Create a repeating alarm
chrome.alarms.create('checkUpdates', { periodInMinutes: 30 });
// Listen for alarm
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'checkUpdates') {
fetchAndNotify();
}
});
Notifications API
chrome.notifications.create('myNotif', {
type: 'basic',
iconUrl: 'icons/icon128.png',
title: 'Heads up!',
message: 'Something interesting happened.',
priority: 2,
});
Web Request / Declarative Net Request
// Modern approach (MV3): Declarative rules in manifest or dynamically
chrome.declarativeNetRequest.updateDynamicRules({
addRules: [{
id: 1,
priority: 1,
action: { type: 'block' },
condition: {
urlFilter: '*://ads.example.com/*',
resourceTypes: ['script', 'image'],
},
}],
removeRuleIds: [1],
});
Building Your First Extension
Let's build a Reading Time Calculator that shows estimated reading time on any article.
Project Structure
reading-time/
├── manifest.json
├── content.js
├── popup.html
├── popup.js
├── popup.css
└── icons/
├── icon16.png
├── icon48.png
└── icon128.png
manifest.json
{
"manifest_version": 3,
"name": "Reading Time",
"version": "1.0.0",
"description": "Shows estimated reading time on any webpage",
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"action": {
"default_popup": "popup.html",
"default_icon": "icons/icon48.png"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"permissions": ["activeTab"]
}
content.js
function calculateReadingTime() {
const text = document.body.innerText;
const words = text.trim().split(/\s+/).length;
const minutes = Math.ceil(words / 200);
// Create badge
const badge = document.createElement('div');
badge.textContent = `${minutes} min read`;
Object.assign(badge.style, {
position: 'fixed',
top: '12px',
right: '12px',
background: '#667eea',
color: 'white',
padding: '8px 16px',
borderRadius: '20px',
fontSize: '14px',
fontFamily: 'system-ui',
fontWeight: '600',
zIndex: '999999',
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
transition: 'opacity 0.3s',
cursor: 'pointer',
});
badge.addEventListener('click', () => {
badge.style.opacity = '0';
setTimeout(() => badge.remove(), 300);
});
document.body.appendChild(badge);
}
// Run after page loads
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', calculateReadingTime);
} else {
calculateReadingTime();
}
Load It Locally
- Open
chrome://extensions/ - Enable Developer mode (toggle in top right)
- Click Load unpacked → select your project folder
- Visit any webpage — you'll see the reading time badge!
Real-World Extension Patterns
Pattern 1: Page Modifier (like Dark Reader)
Inject CSS to transform any page:
// content.js — Dark mode injector
const darkCSS = `
html { filter: invert(1) hue-rotate(180deg); }
img, video, canvas { filter: invert(1) hue-rotate(180deg); }
`;
const style = document.createElement('style');
style.textContent = darkCSS;
document.head.appendChild(style);
Pattern 2: Content Blocker (like uBlock Origin)
Use declarative net request rules:
// rules.json
[
{
"id": 1,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "||ads.example.com",
"resourceTypes": ["script", "image", "sub_frame"]
}
}
]
Pattern 3: Data Scraper / Extractor
// content.js — Extract structured data from a page
function extractProductData() {
return {
title: document.querySelector('h1')?.textContent?.trim(),
price: document.querySelector('.price')?.textContent?.trim(),
rating: document.querySelector('.rating')?.getAttribute('data-value'),
url: window.location.href,
};
}
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'SCRAPE') {
sendResponse(extractProductData());
}
});
Pattern 4: Productivity Tool (like Momentum)
Override the new tab page:
// In manifest.json
{
"chrome_url_overrides": {
"newtab": "newtab.html"
}
}
Storage & State Management
Choosing the Right Storage
| Storage | Capacity | Syncs | Use For |
|---|---|---|---|
chrome.storage.local |
10 MB+ | No | Large data, user preferences |
chrome.storage.sync |
100 KB | Yes (across devices) | Settings, small configs |
chrome.storage.session |
10 MB | No (cleared on close) | Temporary session data |
| IndexedDB | Unlimited | No | Large structured data |
Pro Pattern: Reactive Storage
// storage.js — Shared utility
class ExtStorage {
static async get(keys) {
return chrome.storage.local.get(keys);
}
static async set(data) {
return chrome.storage.local.set(data);
}
static onChange(key, callback) {
chrome.storage.onChanged.addListener((changes) => {
if (changes[key]) {
callback(changes[key].newValue, changes[key].oldValue);
}
});
}
}
// Usage anywhere:
ExtStorage.onChange('enabled', (newVal) => {
console.log('Enabled changed to:', newVal);
});
Messaging — Making Components Talk
Components (popup, content script, service worker) live in different contexts. They communicate via message passing.
┌──────────┐ sendMessage ┌─────────────┐ sendMessage ┌──────────┐
│ Popup │──────────────▶│ Background │──────────────▶│ Content │
│ │◀──────────────│ (Service │◀──────────────│ Script │
│ │ sendResponse │ Worker) │ sendResponse │ │
└──────────┘ └─────────────┘ └──────────┘
// From popup → background
chrome.runtime.sendMessage({ type: 'GET_COUNT' }, (response) => {
console.log('Count:', response.count);
});
// From background → content script (specific tab)
chrome.tabs.sendMessage(tabId, { type: 'HIGHLIGHT', color: 'yellow' });
// In background — listening
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'GET_COUNT') {
chrome.storage.local.get('count', (data) => {
sendResponse({ count: data.count || 0 });
});
return true; // IMPORTANT: keeps channel open for async response
}
});
Permissions — The Trust System
Permissions determine what your extension can access. Users see them before installing, so request only what you need.
Permission Categories
| Type | Example | When to Use |
|---|---|---|
| API permissions |
storage, notifications, alarms
|
Using specific Chrome APIs |
| Host permissions | https://*.github.com/* |
Accessing specific sites |
| activeTab | (no host needed) | Only need current tab on click |
| Optional | Requested at runtime | Non-essential features |
Best Practice: Start with Minimal Permissions
{
"permissions": ["storage", "activeTab"],
"optional_permissions": ["notifications", "bookmarks"],
"optional_host_permissions": ["https://*.example.com/*"]
}
// Request optional permission when needed
chrome.permissions.request(
{ permissions: ['notifications'] },
(granted) => {
if (granted) showNotification();
}
);
Why this matters: Extensions requesting
<all_urls>or too many permissions get flagged during Chrome Web Store review and scare users away.
Debugging Extensions
Essential Debugging Tools
-
Service Worker console:
chrome://extensions/→ click "Inspect views: service worker" - Content Script console: Regular DevTools (F12) on any page → Console tab
- Popup console: Right-click extension icon → "Inspect popup"
- Storage viewer: DevTools → Application tab → Extension Storage
Common Debug Commands
// Check what's in storage
chrome.storage.local.get(null, (data) => console.log('All storage:', data));
// Check active permissions
chrome.permissions.getAll((perms) => console.log('Permissions:', perms));
// Monitor all messages
chrome.runtime.onMessage.addListener((msg, sender) => {
console.log('Message from', sender.tab?.url || 'extension', ':', msg);
});
Publishing to Chrome Web Store
Step-by-Step
Create a developer account at Chrome Web Store Developer Dashboard — one-time $5 fee
-
Prepare your assets:
- Extension ZIP file (no
node_modules, no.git) - Screenshots (1280x800 or 640x400)
- Promo images (440x280 small, 920x680 large)
- Detailed description with keywords
- Extension ZIP file (no
Package your extension:
zip -r extension.zip . -x "*.git*" "node_modules/*" "*.DS_Store"
-
Upload and fill in details:
- Category, language, regions
- Privacy policy URL (required if you handle user data)
- Single purpose description
Submit for review — usually takes 1-3 business days
Tips for Faster Approval
- Request minimal permissions with clear justification
- Include a privacy policy if using
host_permissions - Write a clear "single purpose" description
- Don't bundle unrelated features
- Avoid obfuscated code
Manifest V3 vs V2
MV2 is deprecated. Here's what changed:
| Feature | Manifest V2 | Manifest V3 |
|---|---|---|
| Background | Persistent background page | Service worker (event-driven) |
| Network requests |
webRequest (blocking) |
declarativeNetRequest (rules) |
| Remote code | Allowed (CDN scripts) | Not allowed |
| Content Security | Relaxed | Strict (no eval, no inline scripts) |
| Promise support | Callback-based | Native promises |
Migration Gotchas
- Service workers don't have DOM access — no
document, nowindow - Service workers can be killed — don't rely on global variables, use
chrome.storage - No more
chrome.webRequest.onBeforeRequestfor blocking — use declarative rules
Monetization Strategies
-
Freemium — Free core features, paid premium (most common). Use
chrome.storage.syncto store license keys - One-time purchase — Sell via Gumroad/Stripe, deliver a license key
- Subscription — Monthly access to premium features via your backend
- Sponsorship — Popular open-source extensions can get sponsors
- Chrome Web Store payments — Deprecated. Use your own payment flow
Extensions like Grammarly, Honey (acquired by PayPal for $4B), and Momentum prove that browser extensions can be real businesses.
Common Pitfalls
-
Storing state in service worker globals — They get killed. Use
chrome.storageinstead. -
Forgetting
return truein message listeners — Async responses won't work without it. - Requesting too many permissions — Users won't install. Chrome review will reject.
-
Not handling the case where content script isn't injected — If the user navigates to
chrome://pages, your content script won't run. -
Using
eval()or inline scripts — Blocked by MV3's Content Security Policy. - Not testing on different sites — Your DOM manipulation might break on sites with Shadow DOM or dynamic content.
Let's Connect!
If you found this guide helpful, I'd love to connect with you! I regularly share deep dives on web development, system design, and software engineering.
Connect with me on LinkedIn — let's grow together.
Drop a comment, share this with a fellow developer, and follow along for more guides like this!
Top comments (0)