DEV Community

CsharpDeveloper
CsharpDeveloper

Posted on

How to Build a Chrome Extension with Manifest V3 — Complete Guide

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

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

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">&times;</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; }
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

Step 7: Load and Test

  1. Go to chrome://extensions
  2. Enable Developer mode (top right)
  3. Click Load unpacked
  4. Select your extension folder
  5. Visit any webpage and test

Every time you change code, click the reload button on the extensions page.

Step 8: Publish

  1. ZIP your extension folder
  2. Go to Chrome Web Store Developer Dashboard
  3. Pay $5 one-time registration fee
  4. Upload ZIP, add description and screenshots
  5. 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)