How to Build a Chrome Extension in 2026 (Manifest V3 Guide)
DEV Community

How to Build a Chrome Extension in 2026 (Manifest V3 Guide)

Originally published on the MatrixInn Solutions blog. Chrome extensions give you superpowers over the browser - blocking ads, automating tasks, injecting UI into web pages, capturing screenshots, managing tabs, and much more. The Chrome Web Store has over 100,000 extensions used by hundreds of millions of people. This guide walks you through building a complete Chrome extension from scratch using Manifest V3 (MV3), the current extension platform: it has been required for new Chrome Web Store items since 2022, and Chrome finished switching off Manifest V2 extensions in 2025. What Changed in Manifest V3? If you've followed older tutorials, be careful - most are outdated. Manifest V3 made several breaking changes from MV2: - Background pages → Service Workers. The old persistent background.html is gone. You now use a service worker that runs on demand and terminates when idle. - No remote code execution. You can no longer load JavaScript from external URLs. All code must be bundled in the extension package. - webRequest → declarativeNetRequest. Blocking webRequest is no longer available to regular extensions (only to ones force-installed by enterprise policy). Ad blockers and request modifiers use the declarativeNetRequest API instead. - Promises everywhere. Most chrome.* APIs now return Promises instead of requiring callbacks. Extension Architecture A Chrome extension is made of these components: - manifest.json - The configuration file. Declares permissions, scripts, and extension metadata. - Service Worker (background.js) - Runs in the background. Handles browser events like tab changes, alarms, and messages. - Content Scripts - JavaScript injected into web pages. They can read and modify the DOM but only use a small subset of chrome.* APIs (such as runtime messaging,storage andi18n ); everything else goes through the service worker. - Popup - The HTML/JS/CSS UI shown when the user clicks the extension icon. - Options Page - A full settings page accessible via right-click on the icon. Step 1 - Create manifest.json Every extension starts with a manifest.json file: { "manifest_version": 3, "name": "My Extension", "version": "1.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" }, "background": { "service_worker": "background.js" }, "content_scripts": [ { "matches": ["https://.example.com/"], "js": ["content.js"] } ], "permissions": ["storage", "tabs", "activeTab"], "host_permissions": ["https://.example.com/"] } Key Fields Explained - manifest_version: 3 - Required. Tells Chrome this is an MV3 extension. - action - Defines the toolbar button and popup. - background.service_worker - Path to your background service worker file. - content_scripts - Scripts injected into matching pages. Usematches to control which URLs. - permissions - API permissions likestorage ,tabs ,notifications . - host_permissions - Separate from permissions in MV3. Required to read/modify specific domains. Step 2 - Build the Popup Create popup.html : My Extension Do Something Note: inline scripts ( ... ) are blocked in extension HTML files by CSP. Always use external .js files. Create popup.js : document.getElementById('btn-action').addEventListener('click', async () => { // Query the active tab const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); // Send message to content script const response = await chrome.tabs.sendMessage(tab.id, { action: 'doSomething' }); document.getElementById('status').textContent = response.result; }); Step 3 - Write the Service Worker Create background.js . This runs as a service worker - it will be terminated when idle and restarted on events: // Listen for extension install chrome.runtime.onInstalled.addListener(() => { console.log('Extension installed'); chrome.storage.local.set({ enabled: true }); }); // Listen for messages from popup or content scripts chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === 'getStatus') { chrome.storage.local.get('enabled', (data) => { sendResponse({ enabled: data.enabled }); }); return true; // Keep message channel open for async response } }); Critical MV3 service worker gotcha: Service workers terminate after ~30 seconds of inactivity. Don't store state in global variables - use chrome.storage.local for persistence. Step 4 - Write the Content Script Create content.js : // Listen for messages from popup/background chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === 'doSomething') { // Interact with the page DOM const title = document.title; sendResponse({ result: Page title: ${title} }); } return true; }); Content scripts can read and modify the DOM, but they run in an isolated world - they can't access JavaScript variables set by the page itself. Use chrome.runtime.sendMessage to communicate with the service worker. Step 5 - Storage Use chrome.storage.local (device only) or chrome.storage.sync (synced across user's Chrome instances): // Save await chrome.storage.local.set({ key: 'value' }); // Read const data = await chrome.storage.local.get('key'); console.log(data.key); // 'value' // Remove await chrome.storage.local.remove('key'); Step 6 - Load the Extension Locally - Open Chrome and go to chrome://extensions - Enable Developer mode (toggle in top right) - Click Load unpacked - Select your extension folder - Your extension appears in the toolbar Every time you change code, click the reload button on the extensions page (or Ctrl+R on the extensions page). Step 7 - Common Permissions Reference - "storage" - Read/write local or synced storage - "tabs" - Access tab URLs, titles, and metadata - "activeTab" - Temporary access to the active tab on user gesture (preferred over tabs for most use cases) - "notifications" - Show desktop notifications - "contextMenus" - Add items to the right-click context menu - "alarms" - Schedule periodic background tasks - "clipboardWrite" /"clipboardRead" - Clipboard access - "scripting" - Programmatically inject scripts withchrome.scripting.executeScript Step 8 - Publish to the Chrome Web Store - Create a Chrome Web Store developer account ($5 one-time fee) - Zip your extension folder (not the folder itself, just its contents) - Upload the .zip, add screenshots (1280×800 or 640×400), write a description - Submit for review - most new extensions are reviewed within a few business days; broad permissions can make it take longer Common MV3 Mistakes to Avoid - Using eval() or innerHTML with external content - Blocked by CSP. Use textContent or sanitize carefully. - Storing state in service worker globals - The worker gets killed. Use chrome.storage instead. - Requesting too many permissions - Chrome Web Store reviewers flag this. Only request what you need. - Using inline scripts in HTML files - Not allowed. Always external .js files. - Forgetting return true in message listeners - Needed for async sendResponse, or the channel closes. Next Steps Once you have a working extension, consider these enhancements: - Add an options page for user settings - Use chrome.alarms for periodic background tasks - Implement context menus with chrome.contextMenus - Add keyboard shortcuts via the commands manifest key This article was written with AI assistance and reviewed by the author. Top comments (0)

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.