How to structure a Chrome Extension with Manifest V3 (the right way)
If you've tried building a Chrome extension recently, you've probably hit Manifest V3 and spent an hour just figuring out why your background page stopped working. MV3 replaced background pages with service workers, changed how content scripts communicate, and made permissions stricter. The official docs are... not great. So here's the structure that actually works. The folder structure chrome-extension/ ├── manifest.json ├── popup/ │ ├── popup.html │ ├── popup.css │ └── popup.js ├── options/ │ ├── options.html │ └── options.js ├── content/ │ └── content.js ├── background/ │ └── service-worker.js ├── utils/ │ └── storage.js └── icons/ The manifest.json (MV3) The biggest MV3 gotcha: background scripts are now service workers. { "manifest_version": 3, "name": "Your Extension", "version": "1.0.0", "permissions": ["storage", "activeTab", "scripting"], "action": { "default_popup": "popup/popup.html" }, "background": { "service_worker": "background/service-worker.js" }, "content_scripts": [ { "matches": [""], "js": ["content/content.js"] } ] } Communicating between popup and content script This trips up almost everyone. The popup can't directly access the page DOM — it has to message the content script. // popup.js const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); await chrome.tabs.sendMessage(tab.id, { type: 'RUN_ACTION' }); // content.js chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'RUN_ACTION') { // do something on the page sendResponse({ success: true }); } return true; // keeps the channel open for async response }); The return true at the end is critical — without it, async responses silently fail. Storage that syncs across devices Use chrome.storage.sync instead of localStorage. Here's a utility wrapper that makes it clean to use anywhere: const Storage = { async get(key) { return new Promise((resolve) => { chrome.storage.sync.get([key], (result) => resolve(result[key])); }); }, async set