DEV Community

Ishaan Pandey
Ishaan Pandey

Posted on • Originally published at ishaaan.hashnode.dev

Chrome Extensions 101: The Ultimate Guide to Building, Publishing & Mastering Browser Extensions

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

  1. What Are Chrome Extensions?
  2. The Architecture — How Extensions Work
  3. Manifest.json — The Blueprint
  4. Core Components Deep Dive
  5. Chrome APIs — Your Toolbox
  6. Building Your First Extension
  7. Real-World Extension Patterns
  8. Storage & State Management
  9. Messaging — Making Components Talk
  10. Permissions — The Trust System
  11. Debugging Extensions
  12. Publishing to Chrome Web Store
  13. Manifest V3 vs V2
  14. Monetization Strategies
  15. 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                    │   │
│  └───────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
});
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
// 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,
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

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);
  }
});
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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();
  }
});
Enter fullscreen mode Exit fullscreen mode

Notifications API

chrome.notifications.create('myNotif', {
  type: 'basic',
  iconUrl: 'icons/icon128.png',
  title: 'Heads up!',
  message: 'Something interesting happened.',
  priority: 2,
});
Enter fullscreen mode Exit fullscreen mode

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],
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

Load It Locally

  1. Open chrome://extensions/
  2. Enable Developer mode (toggle in top right)
  3. Click Load unpacked → select your project folder
  4. 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);
Enter fullscreen mode Exit fullscreen mode

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"]
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

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());
  }
});
Enter fullscreen mode Exit fullscreen mode

Pattern 4: Productivity Tool (like Momentum)

Override the new tab page:

// In manifest.json
{
  "chrome_url_overrides": {
    "newtab": "newtab.html"
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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 │          │
└──────────┘               └─────────────┘               └──────────┘
Enter fullscreen mode Exit fullscreen mode
// 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
  }
});
Enter fullscreen mode Exit fullscreen mode

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/*"]
}
Enter fullscreen mode Exit fullscreen mode
// Request optional permission when needed
chrome.permissions.request(
  { permissions: ['notifications'] },
  (granted) => {
    if (granted) showNotification();
  }
);
Enter fullscreen mode Exit fullscreen mode

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

  1. Service Worker console: chrome://extensions/ → click "Inspect views: service worker"
  2. Content Script console: Regular DevTools (F12) on any page → Console tab
  3. Popup console: Right-click extension icon → "Inspect popup"
  4. 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);
});
Enter fullscreen mode Exit fullscreen mode

Publishing to Chrome Web Store

Step-by-Step

  1. Create a developer account at Chrome Web Store Developer Dashboard — one-time $5 fee

  2. 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
  3. Package your extension:

   zip -r extension.zip . -x "*.git*" "node_modules/*" "*.DS_Store"
Enter fullscreen mode Exit fullscreen mode
  1. Upload and fill in details:

    • Category, language, regions
    • Privacy policy URL (required if you handle user data)
    • Single purpose description
  2. 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, no window
  • Service workers can be killed — don't rely on global variables, use chrome.storage
  • No more chrome.webRequest.onBeforeRequest for blocking — use declarative rules

Monetization Strategies

  1. Freemium — Free core features, paid premium (most common). Use chrome.storage.sync to store license keys
  2. One-time purchase — Sell via Gumroad/Stripe, deliver a license key
  3. Subscription — Monthly access to premium features via your backend
  4. Sponsorship — Popular open-source extensions can get sponsors
  5. 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

  1. Storing state in service worker globals — They get killed. Use chrome.storage instead.
  2. Forgetting return true in message listeners — Async responses won't work without it.
  3. Requesting too many permissions — Users won't install. Chrome review will reject.
  4. Not handling the case where content script isn't injected — If the user navigates to chrome:// pages, your content script won't run.
  5. Using eval() or inline scripts — Blocked by MV3's Content Security Policy.
  6. 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)