<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aakanksha</title>
    <description>The latest articles on DEV Community by Aakanksha (@aakanksha_1847236936713a8).</description>
    <link>https://dev.to/aakanksha_1847236936713a8</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4057523%2Fc0b7002f-aa37-4622-b011-02c5e9b910d7.png</url>
      <title>DEV Community: Aakanksha</title>
      <link>https://dev.to/aakanksha_1847236936713a8</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aakanksha_1847236936713a8"/>
    <language>en</language>
    <item>
      <title>How to structure a Chrome Extension with Manifest V3 (the right way)</title>
      <dc:creator>Aakanksha</dc:creator>
      <pubDate>Sat, 01 Aug 2026 06:14:02 +0000</pubDate>
      <link>https://dev.to/aakanksha_1847236936713a8/how-to-structure-a-chrome-extension-with-manifest-v3-the-right-way-4mje</link>
      <guid>https://dev.to/aakanksha_1847236936713a8/how-to-structure-a-chrome-extension-with-manifest-v3-the-right-way-4mje</guid>
      <description>&lt;p&gt;If you've tried building a Chrome extension recently, you've probably &lt;br&gt;
hit Manifest V3 and spent an hour just figuring out why your &lt;br&gt;
background page stopped working.&lt;/p&gt;

&lt;p&gt;MV3 replaced background pages with service workers, changed how &lt;br&gt;
content scripts communicate, and made permissions stricter. The &lt;br&gt;
official docs are... not great. So here's the structure that &lt;br&gt;
actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The folder structure
&lt;/h2&gt;

&lt;p&gt;chrome-extension/&lt;br&gt;
├── manifest.json&lt;br&gt;
├── popup/&lt;br&gt;
│   ├── popup.html&lt;br&gt;
│   ├── popup.css&lt;br&gt;
│   └── popup.js&lt;br&gt;
├── options/&lt;br&gt;
│   ├── options.html&lt;br&gt;
│   └── options.js&lt;br&gt;
├── content/&lt;br&gt;
│   └── content.js&lt;br&gt;
├── background/&lt;br&gt;
│   └── service-worker.js&lt;br&gt;
├── utils/&lt;br&gt;
│   └── storage.js&lt;br&gt;
└── icons/&lt;/p&gt;

&lt;h2&gt;
  
  
  The manifest.json (MV3)
&lt;/h2&gt;

&lt;p&gt;The biggest MV3 gotcha: background scripts are now service workers.&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "manifest_version": 3,&lt;br&gt;
  "name": "Your Extension",&lt;br&gt;
  "version": "1.0.0",&lt;br&gt;
  "permissions": ["storage", "activeTab", "scripting"],&lt;br&gt;
  "action": {&lt;br&gt;
    "default_popup": "popup/popup.html"&lt;br&gt;
  },&lt;br&gt;
  "background": {&lt;br&gt;
    "service_worker": "background/service-worker.js"&lt;br&gt;
  },&lt;br&gt;
  "content_scripts": [&lt;br&gt;
    {&lt;br&gt;
      "matches": [""],&lt;br&gt;
      "js": ["content/content.js"]&lt;br&gt;
    }&lt;br&gt;
  ]&lt;br&gt;
}&lt;/p&gt;

&lt;h2&gt;
  
  
  Communicating between popup and content script
&lt;/h2&gt;

&lt;p&gt;This trips up almost everyone. The popup can't directly access &lt;br&gt;
the page DOM — it has to message the content script.&lt;/p&gt;

&lt;p&gt;// popup.js&lt;br&gt;
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });&lt;br&gt;
await chrome.tabs.sendMessage(tab.id, { type: 'RUN_ACTION' });&lt;/p&gt;

&lt;p&gt;// content.js&lt;br&gt;
chrome.runtime.onMessage.addListener((message, sender, sendResponse) =&amp;gt; {&lt;br&gt;
  if (message.type === 'RUN_ACTION') {&lt;br&gt;
    // do something on the page&lt;br&gt;
    sendResponse({ success: true });&lt;br&gt;
  }&lt;br&gt;
  return true; // keeps the channel open for async response&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;return true&lt;/code&gt; at the end is critical — without it, async &lt;br&gt;
responses silently fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage that syncs across devices
&lt;/h2&gt;

&lt;p&gt;Use chrome.storage.sync instead of localStorage. Here's a &lt;br&gt;
utility wrapper that makes it clean to use anywhere:&lt;/p&gt;

&lt;p&gt;const Storage = {&lt;br&gt;
  async get(key) {&lt;br&gt;
    return new Promise((resolve) =&amp;gt; {&lt;br&gt;
      chrome.storage.sync.get([key], (result) =&amp;gt; resolve(result[key]));&lt;br&gt;
    });&lt;br&gt;
  },&lt;br&gt;
  async set(key, value) {&lt;br&gt;
    return new Promise((resolve) =&amp;gt; {&lt;br&gt;
      chrome.storage.sync.set({ [key]: value }, resolve);&lt;br&gt;
    });&lt;br&gt;
  }&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;// Usage&lt;br&gt;
await Storage.set('theme', 'dark');&lt;br&gt;
const theme = await Storage.get('theme');&lt;/p&gt;

&lt;h2&gt;
  
  
  Service worker gotchas
&lt;/h2&gt;

&lt;p&gt;Service workers in MV3 are not persistent — they sleep when idle &lt;br&gt;
and wake up for events. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don't store state in global variables (it resets)&lt;/li&gt;
&lt;li&gt;Use chrome.storage for anything that needs to persist&lt;/li&gt;
&lt;li&gt;Register event listeners at the top level, not inside callbacks&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Loading it in Chrome
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Go to chrome://extensions&lt;/li&gt;
&lt;li&gt;Enable Developer Mode (top right)&lt;/li&gt;
&lt;li&gt;Click "Load unpacked" → select your folder&lt;/li&gt;
&lt;li&gt;Done&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;I put all of this into a ready-to-run starter kit with a popup UI, &lt;br&gt;
dark mode, options page, and all the wiring done — if you want to &lt;br&gt;
skip setup and go straight to building your feature, it's on &lt;br&gt;
Gumroad: &lt;a href="https://challawar2.gumroad.com/l/chrome-extension-starter-kit" rel="noopener noreferrer"&gt;https://challawar2.gumroad.com/l/chrome-extension-starter-kit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Otherwise, feel free to use the code snippets above — happy to &lt;br&gt;
answer questions in the comments.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>node</category>
    </item>
  </channel>
</rss>
