I Built a Chrome Extension in a Weekend. Here's Exactly How You Can Too.

Why I Built This

I was doom-scrolling YouTube at 2 AM. Again.

Not because I wanted to. I'd opened YouTube to watch one specific tutorial. But the algorithm had other plans — Shorts on the homepage, "recommended for you" in the sidebar, autoplay queuing the next dopamine hit. An hour later, I was watching a guy restore a rusty axe.

So I did what any annoyed developer would do: I built a browser extension to rip all that stuff out. I called it D-Tox.

This post is the guide I wish I had when I started. No theory dumps, no "hello world" that teaches you nothing useful. We're building something real.

What You'll Build

A browser extension that:

  • Runs on specific websites (YouTube in our case)
  • Has a popup UI when you click the icon
  • Stores user preferences that persist across sessions
  • Injects CSS/JS into web pages to modify their behavior

By the end, you'll understand the full anatomy of a browser extension — and you'll have one that works.

The Anatomy of a Browser Extension

Every extension has 4 parts. That's it. Once you understand these, you understand extensions.

1. The Manifest (manifest.json)

This is your extension's ID card. It tells the browser what your extension is called, what permissions it needs, and what files to load.

json
{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0.0",
  "description": "What it does in one line",
  "permissions": ["storage"],
  "action": {
    "default_popup": "popup.html"
  },
  "content_scripts": [
    {
      "matches": ["https://www.youtube.com/*"],
      "js": ["content.js"]
    }
  ]
}

Key things to notice:

  • manifest_version: 3 — Always use v3. v2 is deprecated.
  • permissions — Only ask for what you need. storage lets you save settings.
  • action.default_popup — The HTML file that opens when someone clicks your icon.
  • content_scripts — Code that runs inside the target website's pages.

2. The Popup (Your UI)

This is what users see when they click your extension icon. It's just regular HTML, CSS, and JavaScript. Nothing special.

html
<!DOCTYPE html>
<html>
  <head>
    <style>
      body {
        width: 300px;
        padding: 16px;
        font-family: system-ui;
      }
      .toggle {
        display: flex;
        justify-content: space-between;
        padding: 8px 0;
      }
    </style>
  </head>
  <body>
    <h2>My Extension</h2>
    <div class="toggle">
      <span>Hide Shorts</span>
      <input type="checkbox" id="hideShorts" />
    </div>
    <script src="popup.js"></script>
  </body>
</html>

The popup.js file handles saving/loading settings:

javascript
// Load saved settings when popup opens
chrome.storage.local.get(['hideShorts'], (result) => {
  document.getElementById('hideShorts').checked = result.hideShorts || false;
});
 
// Save when user toggles
document.getElementById('hideShorts').addEventListener('change', (e) => {
  chrome.storage.local.set({ hideShorts: e.target.checked });
});

3. The Content Script (Where the Magic Happens)

This runs inside YouTube's actual page. It can read and modify the DOM — which means you can hide, remove, or change anything on the page.

javascript
// content.js — runs on youtube.com
function applyRules() {
  chrome.storage.local.get(['hideShorts'], (result) => {
    if (result.hideShorts) {
      // Inject a style tag that hides Shorts
      const style = document.createElement('style');
      style.textContent = `
        ytd-reel-shelf-renderer { display: none !important; }
        a[title="Shorts"] { display: none !important; }
      `;
      document.head.appendChild(style);
    }
  });
}
 
applyRules();
 
// YouTube is a Single Page App — watch for navigation
const observer = new MutationObserver(applyRules);
observer.observe(document.body, { childList: true, subtree: true });

The MutationObserver is critical. YouTube doesn't do full page reloads when you navigate. It dynamically swaps content. Without the observer, your rules only apply once and then break.

4. The Background Script (Optional but Useful)

Runs in the background, even when the popup is closed. Good for initialization, handling install events, or cross-tab messaging.

javascript
// background.js
chrome.runtime.onInstalled.addListener((details) => {
  if (details.reason === 'install') {
    // Set default settings on first install
    chrome.storage.local.set({
      hideShorts: true,
      hideComments: false,
    });
  }
});

The Build: Step by Step

Step 1: Create your project folder

plaintext
my-extension/
├── manifest.json
├── popup.html
├── popup.js
├── content.js
├── background.js
└── icon-128.png

Step 2: Write your manifest.json

Start minimal. You can always add more later.

json
{
  "manifest_version": 3,
  "name": "YouTube Cleaner",
  "version": "1.0.0",
  "description": "Hide distracting elements on YouTube",
  "permissions": ["storage", "activeTab"],
  "host_permissions": ["https://www.youtube.com/*"],
  "action": {
    "default_popup": "popup.html",
    "default_title": "YouTube Cleaner"
  },
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["https://www.youtube.com/*"],
      "js": ["content.js"]
    }
  ],
  "icons": {
    "128": "icon-128.png"
  }
}

Step 3: Load it in your browser

  1. Open chrome://extensions/
  2. Enable Developer mode (top right toggle)
  3. Click Load unpacked
  4. Select your project folder

That's it. Your extension is running. Click the icon in the toolbar to see the popup.

Step 4: Make changes and reload

Every time you edit a file:

  1. Go to chrome://extensions/
  2. Click the reload icon on your extension card
  3. Refresh the YouTube tab

No build step, no compilation. Just edit, reload, test.

Leveling Up: React, TypeScript, and WXT

Plain HTML/JS works fine for simple extensions. But if you want:

  • Multiple toggles with state management
  • Type safety
  • Cross-browser support (Firefox, Edge)
  • Hot module reloading during development

Then use WXT — a framework that handles all the extension boilerplate:

bash
npm init -y
npm install wxt react react-dom typescript

WXT gives you:

  • Auto-generated manifest (handles v2/v3 differences)
  • Dev server with HMR
  • Builds for Chrome, Firefox, Edge from the same codebase
  • Clean entrypoint-based file structure

This is what I used for D-Tox. The productivity gain is massive once you have more than 3-4 features.

The Gotchas Nobody Tells You

1. YouTube's DOM changes constantly. The CSS selectors that work today might break next month. Use stable selectors like [is-shorts], #comments, element IDs — not generated class names.

2. Content scripts run in an isolated world. They can access the DOM but not the page's JavaScript variables. If you need to interact with YouTube's player API, you have to inject a script tag.

3. chrome.storage is async. Unlike localStorage, you must use callbacks or await. Many beginners get tripped up by this.

4. Popups die when closed. Your popup's state is destroyed every time the user clicks away. Always persist state to chrome.storage, never rely on in-memory variables in the popup.

5. Test on a real YouTube page. The extension popup works fine in isolation, but the content script needs the actual YouTube DOM to work with.

What I Learned

Building D-Tox taught me that browser extensions are one of the most underrated things you can build as a developer. They're:

  • Immediately useful — You use your own tool every day
  • Shareable — Anyone can install it
  • Small in scope — You can build a real product in a weekend
  • Great portfolio pieces — Shows you can ship, not just code

The barrier to entry is surprisingly low. If you can write HTML, CSS, and JavaScript, you can build an extension.

What's Next

In the next post, I'll walk you through how to publish your extension to the Chrome Web Store so anyone in the world can install it with one click. It's simpler than you think — but there are a few things you need to get right.

The full source code for D-Tox is open source: github.com/najmushsaaquib/d-tox


If this helped you, I'd love to hear what you build. Reach out — I'm always up for talking about things that respect people's attention.

Back to Blog