DEV Community

Ryu0705
Ryu0705

Posted on

Building Chrome Extensions in 2026: A Practical Guide with Manifest V3

Chrome extensions remain one of the most accessible ways to ship useful developer tools. With Manifest V3 now fully enforced, here's a practical guide to building extensions the right way in 2026.

Why Manifest V3?

Manifest V3 replaced the old V2 format with a focus on security, privacy, and performance. The key changes include:

  • Service workers instead of persistent background pages
  • Declarative net request instead of webRequest blocking
  • Tighter permissions with explicit host permissions

If you're starting a new extension today, V3 is the only option.

Project Structure

A typical Manifest V3 extension looks like this:

my-extension/
├── manifest.json
├── background.js        # Service worker
├── content.js           # Content script
├── popup/
│   ├── popup.html
│   └── popup.js
├── options/
│   ├── options.html
│   └── options.js
└── icons/
    ├── icon-16.png
    ├── icon-48.png
    └── icon-128.png
Enter fullscreen mode Exit fullscreen mode

The Manifest File

Here's a minimal but complete manifest.json:

{
  "manifest_version": 3,
  "name": "My Developer Tool",
  "version": "1.0.0",
  "description": "A helpful developer tool",
  "permissions": ["storage", "activeTab"],
  "host_permissions": ["<all_urls>"],
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "run_at": "document_idle"
    }
  ],
  "action": {
    "default_popup": "popup/popup.html",
    "default_icon": {
      "16": "icons/icon-16.png",
      "48": "icons/icon-48.png",
      "128": "icons/icon-128.png"
    }
  },
  "options_ui": {
    "page": "options/options.html",
    "open_in_tab": false
  }
}
Enter fullscreen mode Exit fullscreen mode

Understanding the Core Components

Content Scripts: Interact with Web Pages

Content scripts run in the context of web pages. They can read and modify the DOM but live in an isolated JavaScript environment.

// content.js
// Detect JSON responses and format them
const body = document.body;
const text = body?.innerText;

if (isJsonContent(text)) {
  const parsed = JSON.parse(text);
  renderFormattedJson(parsed, body);
}

function isJsonContent(text) {
  try {
    JSON.parse(text);
    return document.contentType === 'application/json';
  } catch {
    return false;
  }
}

function renderFormattedJson(data, container) {
  const pre = document.createElement('pre');
  pre.textContent = JSON.stringify(data, null, 2);
  pre.style.cssText = 'font-family: monospace; padding: 16px;';
  container.innerHTML = '';
  container.appendChild(pre);
}
Enter fullscreen mode Exit fullscreen mode

Popup: Quick Actions UI

The popup appears when users click your extension icon. Keep it focused and fast.

<!-- popup/popup.html -->
<!DOCTYPE html>
<html>
<head>
  <style>
    body { width: 300px; padding: 16px; font-family: system-ui; }
    .toggle { display: flex; justify-content: space-between; align-items: center; }
    button { padding: 8px 16px; cursor: pointer; }
  </style>
</head>
<body>
  <h3>My Developer Tool</h3>
  <div class="toggle">
    <span>Enable formatting</span>
    <button id="toggleBtn">ON</button>
  </div>
  <script src="popup.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode
// popup/popup.js
const btn = document.getElementById('toggleBtn');

// Load saved state
chrome.storage.sync.get(['enabled'], (result) => {
  const enabled = result.enabled !== false;
  btn.textContent = enabled ? 'ON' : 'OFF';
});

// Toggle on click
btn.addEventListener('click', () => {
  chrome.storage.sync.get(['enabled'], (result) => {
    const newState = result.enabled === false;
    chrome.storage.sync.set({ enabled: newState });
    btn.textContent = newState ? 'ON' : 'OFF';
  });
});
Enter fullscreen mode Exit fullscreen mode

Options Page: User Preferences

The options page lets users configure your extension. It opens from chrome://extensions or right-clicking the extension icon.

// options/options.js
const themeSelect = document.getElementById('theme');
const indentInput = document.getElementById('indent');

// Load preferences
chrome.storage.sync.get(['theme', 'indent'], (result) => {
  themeSelect.value = result.theme || 'dark';
  indentInput.value = result.indent || 2;
});

// Save on change
document.getElementById('save').addEventListener('click', () => {
  chrome.storage.sync.set({
    theme: themeSelect.value,
    indent: parseInt(indentInput.value, 10)
  });
});
Enter fullscreen mode Exit fullscreen mode

Service Worker: Background Logic

The service worker handles events, manages state, and coordinates between components.

// background.js
chrome.runtime.onInstalled.addListener((details) => {
  if (details.reason === 'install') {
    chrome.storage.sync.set({ enabled: true, theme: 'dark', indent: 2 });
  }
});

// Listen for messages from content scripts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'GET_SETTINGS') {
    chrome.storage.sync.get(['enabled', 'theme', 'indent'], (settings) => {
      sendResponse(settings);
    });
    return true; // Keep channel open for async response
  }
});
Enter fullscreen mode Exit fullscreen mode

Permission Minimization Best Practices

One of V3's goals is reducing excessive permissions. Follow these rules:

1. Use activeTab instead of broad host permissions when possible

// Bad: Requests access to all sites on install
"host_permissions": ["<all_urls>"]

// Good: Only activates when user clicks the extension
"permissions": ["activeTab"]
Enter fullscreen mode Exit fullscreen mode

2. Request optional permissions at runtime

// Request permissions only when needed
chrome.permissions.request(
  { origins: ['https://api.example.com/*'] },
  (granted) => {
    if (granted) {
      fetchData();
    }
  }
);
Enter fullscreen mode Exit fullscreen mode

3. Only declare what you use

Every permission you list appears in the Chrome Web Store listing. Users are more likely to install extensions with fewer permissions.

Permission Use When
storage Saving user preferences
activeTab Accessing the current tab on click
tabs Reading tab URLs (consider if you really need this)
alarms Scheduling periodic tasks

Debugging and Testing

Loading Your Extension

  1. Open chrome://extensions
  2. Enable Developer mode (top right)
  3. Click Load unpacked and select your project folder
  4. Your extension appears in the toolbar

Debugging Tips

Service worker logs: Click "Inspect views: service worker" on the extensions page.

Content script logs: Open DevTools on any page where your content script runs. Logs appear in the regular console.

Popup debugging: Right-click your extension icon, click "Inspect popup". This opens DevTools for the popup context.

Testing Strategy

// Use a simple test runner for unit tests
// test/format.test.js
import { describe, it, expect } from 'vitest';
import { isJsonContent, formatJson } from '../src/utils.js';

describe('JSON detection', () => {
  it('detects valid JSON strings', () => {
    expect(isJsonContent('{"key": "value"}')).toBe(true);
  });

  it('rejects invalid JSON', () => {
    expect(isJsonContent('not json')).toBe(false);
  });
});

describe('JSON formatting', () => {
  it('formats with custom indent', () => {
    const input = '{"a":1}';
    const result = formatJson(input, 4);
    expect(result).toContain('    "a"');
  });
});
Enter fullscreen mode Exit fullscreen mode

For end-to-end testing, use Puppeteer with extension support:

const browser = await puppeteer.launch({
  headless: false,
  args: [
    `--disable-extensions-except=${extensionPath}`,
    `--load-extension=${extensionPath}`
  ]
});
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls

  1. Service worker lifecycle: Unlike background pages, service workers terminate when idle. Don't store state in global variables; use chrome.storage instead.

  2. Content Security Policy: V3 disallows inline scripts in extension pages. Move all JavaScript to separate files.

  3. Message passing: Always return true from onMessage listeners if you call sendResponse asynchronously.

Wrapping Up

Manifest V3 enforces better security patterns that ultimately lead to better extensions. Start with minimal permissions, use the component model (content script, popup, options, service worker) to separate concerns, and test early.

If you want to see these patterns in action, check out JSON Formatter Pro - an open-source Chrome extension that auto-detects and beautifully formats JSON in your browser, built with all the Manifest V3 best practices covered in this guide.


Have questions about Chrome extension development? Drop them in the comments!

Top comments (0)