{"post":{"slug":"how-to-build-a-chrome-extension-2026","title":"How to build a Chrome extension in 2026","excerpt":"Chrome extensions are more capable and more restricted than ever in 2026 with Manifest V3 fully enforced. This guide covers the architecture, permissions, and build tooling to ship one correctly.","category":"code","date":"March 11, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">C</span>hrome extensions in 2026 all run under Manifest V3, and the transition from MV2 has changed the architecture significantly. Service workers replaced persistent background pages, `declarativeNetRequest` replaced blocking `webRequest`, and the permission model got stricter. Once you understand the MV3 model, building an extension is straightforward. Here is the complete path.\n\n## What changed in 2026\n\n- **MV2 is fully disabled** in Chrome stable. The migration period ended; all extensions must be MV3.\n- **Side panel API** (stable since Chrome 116) lets extensions show a persistent panel alongside the page — the replacement for many use cases that previously used popups.\n- **`chrome.offscreen`** API solves the \"service workers cannot access the DOM\" problem for cases like audio playback or clipboard access.\n- **CRXJS Vite plugin** matured to v2 and is now the standard build setup for React/TypeScript extensions.\n\n## Extension architecture (MV3)\n\n```\nmy-extension/\n├── manifest.json          # Extension metadata and permissions\n├── background/\n│   └── service-worker.ts  # Ephemeral background logic\n├── content/\n│   └── content-script.ts  # Runs in the context of web pages\n├── popup/\n│   ├── popup.html\n│   └── popup.tsx          # React popup UI\n├── options/\n│   └── options.tsx        # Settings page\n└── icons/\n    └── icon-128.png\n```\n\n## manifest.json (MV3)\n\n```json\n{\n  \"manifest_version\": 3,\n  \"name\": \"My Extension\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Does a specific useful thing.\",\n  \"permissions\": [\"storage\", \"activeTab\"],\n  \"host_permissions\": [\"https://example.com/*\"],\n  \"background\": {\n    \"service_worker\": \"background/service-worker.js\",\n    \"type\": \"module\"\n  },\n  \"content_scripts\": [\n    {\n      \"matches\": [\"https://example.com/*\"],\n      \"js\": [\"content/content-script.js\"],\n      \"run_at\": \"document_idle\"\n    }\n  ],\n  \"action\": {\n    \"default_popup\": \"popup/popup.html\",\n    \"default_icon\": \"icons/icon-128.png\"\n  },\n  \"icons\": {\n    \"128\": \"icons/icon-128.png\"\n  }\n}\n```\n\n## Build setup with Vite + CRXJS\n\n```bash\nnpm create vite@latest my-extension -- --template react-ts\nnpm install -D @crxjs/vite-plugin\n```\n\n```ts\n// vite.config.ts\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport { crx } from '@crxjs/vite-plugin';\nimport manifest from './manifest.json';\n\nexport default defineConfig({\n  plugins: [react(), crx({ manifest })],\n});\n```\n\nDevelopment with hot reload:\n\n```bash\nnpm run dev\n# Load the `dist/` folder in chrome://extensions as an unpacked extension\n```\n\n## Service worker (background)\n\nService workers in MV3 terminate after ~30 seconds of inactivity. Do not store state in variables — use `chrome.storage`:\n\n```ts\n// background/service-worker.ts\nchrome.runtime.onInstalled.addListener(() => {\n  chrome.storage.local.set({ count: 0 });\n});\n\nchrome.action.onClicked.addListener(async (tab) => {\n  const { count } = await chrome.storage.local.get('count');\n  await chrome.storage.local.set({ count: count + 1 });\n\n  // Send message to content script\n  await chrome.tabs.sendMessage(tab.id!, { type: 'INCREMENT', count: count + 1 });\n});\n```\n\n## Content script\n\n```ts\n// content/content-script.ts\nchrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {\n  if (message.type === 'INCREMENT') {\n    console.log('Count:', message.count);\n    sendResponse({ ok: true });\n  }\n});\n```\n\nContent scripts run in an isolated world — they can access the DOM but not page JavaScript variables. For two-way communication between the page JS and extension, use `window.postMessage`.\n\n## Popup with React\n\n```tsx\n// popup/popup.tsx\nimport { useEffect, useState } from 'react';\n\nexport default function Popup() {\n  const [count, setCount] = useState(0);\n\n  useEffect(() => {\n    chrome.storage.local.get('count').then(({ count }) => setCount(count ?? 0));\n  }, []);\n\n  return (\n    <div style={{ width: 200, padding: 16 }}>\n      <p>Count: {count}</p>\n      <button onClick={() => chrome.action.setBadgeText({ text: String(count) })}>\n        Show badge\n      </button>\n    </div>\n  );\n}\n```\n\n## Permission comparison\n\n| Permission | What it allows | Scrutiny level |\n| --- | --- | --- |\n| `storage` | Read/write extension local storage | Low |\n| `activeTab` | Access the current tab on user gesture | Low |\n| `tabs` | Access all tab URLs and titles | High |\n| `<all_urls>` host permission | Run content scripts everywhere | Very high |\n| `webRequest` | Observe (not block) network requests | High |\n| `declarativeNetRequest` | Block/redirect requests by rules | Medium |\n\nRequest the minimum. Use `activeTab` + `host_permissions` for specific domains instead of `<all_urls>`.\n\n## How to pick your extension type\n\n1. **Action popup + storage?** → Simplest shape; good for productivity tools that act on the current page.\n2. **Persistent sidebar?** → Use the Side Panel API (`chrome.sidePanel`).\n3. **Content transformation (ad block, page modification)?** → Content script + `declarativeNetRequest`.\n4. **Background data sync?** → Service worker with `chrome.alarms` for periodic wakeup.\n\n## Common mistakes\n\n**Storing state in service worker variables.** The worker terminates; variables are lost. Always use `chrome.storage.local` or `chrome.storage.session`.\n\n**Using `fetch` in content scripts cross-origin.** Content scripts share the page origin; cross-origin requests need to go through the service worker via `chrome.runtime.sendMessage`.\n\n**Not handling the service worker dormancy.** If your popup sends a message to the service worker and it has terminated, the message fails. Wrap in a retry or use `chrome.runtime.getBackgroundPage` wakeup pattern.\n\n**Over-requesting permissions at install.** Use `chrome.permissions.request` to ask for optional permissions at the moment the user needs them.\n\n## What to skip\n\n- **Persistent background page patterns from MV2.** There is no persistent background in MV3; architect around service worker termination from the start.\n- **`eval()` in extension code.** The Content Security Policy for MV3 disallows it. Use compiled code.\n- **Publishing to the Chrome Web Store without a privacy policy.** Extensions that access any user data require a linked privacy policy.\n\n## FAQ\n\n**Does Firefox support MV3?**\nYes — Firefox supports MV3, but with some API differences (e.g., `browser.` namespace vs `chrome.`). Use `webextension-polyfill` for cross-browser support.\n\n**How long does Chrome Web Store review take?**\nTypically 1–3 business days for new submissions; updates are often auto-approved in hours if the permissions do not change.\n\n**Can I use TypeScript and React in an extension?**\nYes — the Vite + CRXJS setup above supports both. The popup, options page, and content scripts can all be TypeScript/React files.\n\n**How do I debug a service worker?**\nGo to `chrome://extensions`, find your extension, and click \"Service Worker\" to open DevTools for the service worker context.\n\n## Where to go next\n\n- [How to write a GitHub Action in 2026](/blog/how-to-write-a-github-action-2026)\n- [How to deploy a static site in 2026](/blog/how-to-deploy-a-static-site-2026)\n- [How to publish an npm package in 2026](/blog/how-to-publish-an-npm-package-2026)","hue":314,"metaTitle":"How to Build a Chrome Extension in 2026: Manifest V3 Guide","metaDescription":"Complete guide to building a Chrome extension in 2026 with Manifest V3. Covers service workers, content scripts, permissions, Vite build setup, and publishing.","keywords":["how to build a chrome extension 2026","chrome extension manifest v3 2026","chrome extension service worker","mv3 chrome extension tutorial","chrome extension vite build","chrome extension permissions 2026","content script chrome extension","chrome extension popup react","publish chrome extension 2026","chrome web store submission 2026"],"tldr":["**Manifest V3 is mandatory** — MV2 extensions are disabled in Chrome; all new extensions must use MV3 service workers and declarativeNetRequest.","**Use a build tool** — Vite or Webpack with `crxjs` bundles your extension and handles hot reload during development.","**Service workers replace background pages** — they are ephemeral and terminate after inactivity; do not store state in memory.","**Request only the permissions you need** — the Chrome Web Store reviewer and users both scrutinize wide permissions; narrow them as much as possible.","**Skip:** using MV2 patterns in an MV3 extension — persistent background pages, `XMLHttpRequest` in service workers, and blocking `webRequest` do not exist in MV3."],"readTime":"4 min read","wordCount":971},"related":[{"slug":"how-to-build-chrome-extension-2026","title":"How to build a Chrome extension in 2026 (Manifest V3 done right)","excerpt":"Build a Chrome extension on Manifest V3 in 2026. The architecture, the gotchas, and how to ship without surprises.","category":"code","date":"January 29, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":305},{"slug":"how-to-build-vscode-extension-2026","title":"How to build a VS Code extension in 2026 (a working tutorial)","excerpt":"Build, test, and publish a VS Code extension in 2026. The current API, the publishing flow, and the gotchas.","category":"code","date":"January 28, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":315},{"slug":"how-to-build-mobile-app-2026","title":"How to build a mobile app in 2026: React Native vs Flutter vs Expo","excerpt":"Build a real mobile app in 2026 without native expertise. Honest picks for React Native, Flutter, Expo, and when native still wins.","category":"code","date":"January 29, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":310}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-build-a-budget-spreadsheet-2026","title":"How to build a budget spreadsheet in 2026: a practical zero-fluff template","excerpt":"A budget spreadsheet you actually use beats any app you abandon by March. Here is exactly how to build one from scratch in under an hour.","category":"finance","date":"March 11, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":320},"older":{"slug":"how-to-build-a-reading-habit-2026","title":"How to build a reading habit in 2026: a system that actually sticks","excerpt":"Most reading habits fail because they start with motivation instead of structure. This 2026 guide shows you a system that makes reading automatic.","category":"guides","date":"March 11, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":125}}