Build a Chrome Extension with AI: Step-by-Step Guide for Vibe Coders

You can build something people actually use — today. Not a practice project. Not a tutorial exercise. A real Chrome extension that lives in your browser toolbar, saves your open tabs with one click, and works every single day. Let's build it.

TL;DR

You'll build a "Tab Saver" Chrome extension that saves all your open tabs to a named session with one click and restores them later. Four files: manifest.json, popup.html, popup.js, and a 16x16 icon. Manifest V3, no frameworks, no build tools. Load it in Chrome's developer mode in under 2 minutes.

Why AI Coders Need to Know This

Chrome extensions are one of the most practical, impressive, and underrated things you can build as a vibe coder. Here's why:

  • People actually use them. Not "check out my portfolio" — a tool that sits in someone's browser and helps them every day.
  • They're just web tech. HTML, CSS, JavaScript — the same stuff you've been learning. No new language, no exotic framework.
  • They're small. Most useful extensions are 3-5 files. AI can generate the entire thing in one conversation.
  • They solve real problems. Tab management, productivity shortcuts, page modifications — things you personally need.
  • You can publish them. The Chrome Web Store is right there. A $5 developer account and you're distributing software to millions of potential users.

The secret that experienced developers don't tell you: Chrome extensions have an absurdly simple architecture. A manifest.json file that describes your extension, an HTML popup, and some JavaScript. That's it. If you've built a landing page, you already have 80% of the skills you need.

The remaining 20% is understanding how Chrome's extension APIs work — and that's exactly what AI is brilliant at. Your AI knows every Chrome API, every permission, every manifest option. You just need to know enough to tell it what to build and catch it when it makes mistakes.

Real Scenario

It's Tuesday afternoon. You've got 23 tabs open — research for a client project, a YouTube tutorial you're halfway through, three Stack Overflow threads, your email, your project management tool. You need to switch contexts and work on something else. You don't want to lose those tabs. You don't want to bookmark 23 things individually.

So you open Cursor and type:

Your Prompt

"Build me a Chrome extension that saves all my open tabs as a named session. I want a popup where I type a session name, click Save, and it stores every tab URL. Then I can see my saved sessions and click Restore to open them all back up. Use Manifest V3."

Thirty seconds later, Claude generates four files. You load them into Chrome. It works. You just built a tool that you'll use every day — in the time it takes to make coffee.

Let's walk through exactly what AI generates, what each piece does, and what to watch out for.

What AI Generated

Here's the complete, working code your AI will produce. We'll break down every piece afterward, but first — here's the full picture so you can see how small a Chrome extension actually is.

File 1: manifest.json

This is your extension's ID card. Chrome reads this file to understand what your extension is, what it can do, and what permissions it needs.

{
  "manifest_version": 3,
  "name": "Tab Saver",
  "version": "1.0.0",
  "description": "Save and restore browser tab sessions with one click.",
  "permissions": [
    "tabs",
    "storage"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icon16.png",
      "48": "icon48.png",
      "128": "icon128.png"
    }
  },
  "icons": {
    "16": "icon16.png",
    "48": "icon48.png",
    "128": "icon128.png"
  }
}

File 2: popup.html

This is the little window that appears when you click your extension icon. Regular HTML — nothing special. Notice the <script> tag at the bottom that loads our JavaScript.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Tab Saver</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      width: 350px;
      min-height: 400px;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI',
                   Roboto, sans-serif;
      background: #1a1a2e;
      color: #e0e0e0;
      padding: 16px;
    }

    h1 {
      font-size: 18px;
      margin-bottom: 12px;
      color: #00d4ff;
    }

    .save-section {
      display: flex;
      gap: 8px;
      margin-bottom: 16px;
    }

    .save-section input {
      flex: 1;
      padding: 8px 12px;
      border: 1px solid #333;
      border-radius: 6px;
      background: #16213e;
      color: #e0e0e0;
      font-size: 14px;
      outline: none;
    }

    .save-section input:focus {
      border-color: #00d4ff;
    }

    .save-section button {
      padding: 8px 16px;
      background: #00d4ff;
      color: #1a1a2e;
      border: none;
      border-radius: 6px;
      font-weight: 600;
      cursor: pointer;
      font-size: 14px;
      white-space: nowrap;
    }

    .save-section button:hover {
      background: #00b8d9;
    }

    .sessions-header {
      font-size: 14px;
      color: #888;
      margin-bottom: 8px;
      text-transform: uppercase;
      letter-spacing: 1px;
    }

    .session-list {
      list-style: none;
    }

    .session-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 10px 12px;
      background: #16213e;
      border-radius: 8px;
      margin-bottom: 8px;
    }

    .session-info {
      flex: 1;
    }

    .session-name {
      font-weight: 600;
      font-size: 14px;
    }

    .session-meta {
      font-size: 12px;
      color: #888;
      margin-top: 2px;
    }

    .session-actions {
      display: flex;
      gap: 6px;
    }

    .btn-restore {
      padding: 5px 10px;
      background: #00d4ff;
      color: #1a1a2e;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 12px;
      font-weight: 600;
    }

    .btn-delete {
      padding: 5px 10px;
      background: #ff4757;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 12px;
      font-weight: 600;
    }

    .empty-state {
      text-align: center;
      padding: 24px;
      color: #555;
      font-size: 14px;
    }

    .status-msg {
      text-align: center;
      padding: 8px;
      margin-bottom: 12px;
      border-radius: 6px;
      font-size: 13px;
      display: none;
    }

    .status-msg.success {
      display: block;
      background: #00d4ff22;
      color: #00d4ff;
    }

    .tab-count {
      background: #00d4ff33;
      color: #00d4ff;
      padding: 4px 10px;
      border-radius: 12px;
      font-size: 12px;
      margin-bottom: 16px;
      display: inline-block;
    }
  </style>
</head>
<body>
  <h1>📄 Tab Saver</h1>
  <div id="tab-count" class="tab-count"></div>
  <div id="status" class="status-msg"></div>

  <div class="save-section">
    <input type="text" id="session-name"
           placeholder="Session name (e.g., Work Research)">
    <button id="save-btn">Save Tabs</button>
  </div>

  <div class="sessions-header">Saved Sessions</div>
  <ul id="session-list" class="session-list"></ul>

  <script src="popup.js"></script>
</body>
</html>

File 3: popup.js

This is where the magic happens. This script handles saving tabs, restoring sessions, and managing your saved data using Chrome's storage API.

// popup.js — Tab Saver extension logic

document.addEventListener('DOMContentLoaded', async () => {
  const sessionNameInput = document.getElementById('session-name');
  const saveBtn = document.getElementById('save-btn');
  const sessionList = document.getElementById('session-list');
  const statusEl = document.getElementById('status');
  const tabCountEl = document.getElementById('tab-count');

  // Show current tab count
  const currentTabs = await chrome.tabs.query(
    { currentWindow: true }
  );
  tabCountEl.textContent = `${currentTabs.length} tabs open now`;

  // Load and display saved sessions
  async function loadSessions() {
    const data = await chrome.storage.local.get('sessions');
    const sessions = data.sessions || [];
    sessionList.innerHTML = '';

    if (sessions.length === 0) {
      sessionList.innerHTML =
        '<li class="empty-state">No saved sessions yet.</li>';
      return;
    }

    sessions.forEach((session, index) => {
      const li = document.createElement('li');
      li.className = 'session-item';
      li.innerHTML = `
        <div class="session-info">
          <div class="session-name">${escapeHtml(session.name)}</div>
          <div class="session-meta">
            ${session.tabs.length} tabs · Saved ${formatDate(session.date)}
          </div>
        </div>
        <div class="session-actions">
          <button class="btn-restore" data-index="${index}">
            Restore
          </button>
          <button class="btn-delete" data-index="${index}">
            Delete
          </button>
        </div>
      `;
      sessionList.appendChild(li);
    });
  }

  // Save current tabs as a new session
  saveBtn.addEventListener('click', async () => {
    const name = sessionNameInput.value.trim();
    if (!name) {
      showStatus('Please enter a session name.');
      return;
    }

    const tabs = await chrome.tabs.query(
      { currentWindow: true }
    );
    const tabData = tabs.map(tab => ({
      url: tab.url,
      title: tab.title
    }));

    const data = await chrome.storage.local.get('sessions');
    const sessions = data.sessions || [];

    sessions.unshift({
      name: name,
      tabs: tabData,
      date: new Date().toISOString()
    });

    await chrome.storage.local.set({ sessions });
    sessionNameInput.value = '';
    showStatus(`Saved "${name}" with ${tabData.length} tabs!`);
    loadSessions();
  });

  // Handle restore and delete clicks (event delegation)
  sessionList.addEventListener('click', async (e) => {
    const target = e.target;
    const index = parseInt(target.dataset.index);

    if (target.classList.contains('btn-restore')) {
      const data = await chrome.storage.local.get('sessions');
      const session = data.sessions[index];

      for (const tab of session.tabs) {
        // Skip chrome:// URLs — can't open those programmatically
        if (!tab.url.startsWith('chrome://')) {
          await chrome.tabs.create({ url: tab.url });
        }
      }
      showStatus(
        `Restored "${session.name}" (${session.tabs.length} tabs)`
      );
    }

    if (target.classList.contains('btn-delete')) {
      const data = await chrome.storage.local.get('sessions');
      const sessions = data.sessions || [];
      const removed = sessions.splice(index, 1);
      await chrome.storage.local.set({ sessions });
      showStatus(`Deleted "${removed[0].name}"`);
      loadSessions();
    }
  });

  // Allow pressing Enter to save
  sessionNameInput.addEventListener('keypress', (e) => {
    if (e.key === 'Enter') saveBtn.click();
  });

  // Utility: show status message
  function showStatus(msg) {
    statusEl.textContent = msg;
    statusEl.className = 'status-msg success';
    setTimeout(() => {
      statusEl.className = 'status-msg';
    }, 3000);
  }

  // Utility: escape HTML to prevent XSS
  function escapeHtml(str) {
    const div = document.createElement('div');
    div.textContent = str;
    return div.innerHTML;
  }

  // Utility: format date
  function formatDate(isoString) {
    const date = new Date(isoString);
    const now = new Date();
    const diffMs = now - date;
    const diffMins = Math.floor(diffMs / 60000);

    if (diffMins < 1) return 'just now';
    if (diffMins < 60) return `${diffMins}m ago`;
    const diffHrs = Math.floor(diffMins / 60);
    if (diffHrs < 24) return `${diffHrs}h ago`;
    const diffDays = Math.floor(diffHrs / 24);
    if (diffDays < 7) return `${diffDays}d ago`;
    return date.toLocaleDateString();
  }

  // Initial load
  loadSessions();
});

That's the entire extension. Three files (plus an icon). Let's understand each piece.

Understanding Each Part

A Chrome extension has a clear architecture. Once you understand the pieces, you can build any extension. Here's how they fit together:

manifest.json — The Blueprint

Think of manifest.json as your extension's blueprint (if you're from construction, think of it as the permit application). Chrome reads this file first to understand:

  • "manifest_version": 3 — Which version of Chrome's extension platform you're using. Always use 3. Version 2 is dead.
  • "name" and "version" — What your extension is called and its version number. Chrome uses the version for updates.
  • "permissions" — What your extension is allowed to do. This is the most important part:
    • "tabs" — Lets you read tab URLs and titles, create new tabs, and query which tabs are open.
    • "storage" — Lets you save data that persists between browser sessions (like localStorage but for extensions).
  • "action" — Defines what happens when the user clicks your extension icon. "default_popup" tells Chrome to open popup.html as a small window.

The JSON format is strict — one misplaced comma and Chrome refuses to load your extension. AI usually gets JSON right, but watch for trailing commas (JSON doesn't allow them, unlike JavaScript).

popup.html — The User Interface

The popup is just a regular HTML page with some constraints:

  • Fixed width: Chrome popups max out at 800px wide and 600px tall. We set width: 350px in CSS for a clean look.
  • No inline scripts: In Manifest V3, you cannot put JavaScript directly in your HTML with inline <script> tags or onclick="..." attributes. You must use a separate .js file. This is a security rule called Content Security Policy (CSP) — and it's the #1 thing AI gets wrong.
  • Self-contained styling: The CSS is in a <style> tag right in the HTML. For a small extension, this is totally fine. No need for a separate CSS file.

popup.js — The Brain

This file does all the work. Let's walk through the key patterns:

  • chrome.tabs.query() — Asks Chrome for a list of tabs. We pass { currentWindow: true } to get only tabs in the current window (not other windows).
  • chrome.storage.local — Chrome's built-in storage for extensions. Works like localStorage but better — it's async, has more space (up to 10MB), and syncs across Chrome profiles if you use chrome.storage.sync instead.
  • chrome.tabs.create() — Opens a new tab with the given URL. We use this to restore saved sessions.
  • Event delegation — Instead of adding a click listener to every button, we add one listener to the parent <ul> and check which button was clicked. This is a clean pattern that works even when buttons are added dynamically.
  • escapeHtml() — This prevents XSS attacks. If someone saved a session with a name like <script>alert('hacked')</script>, we don't want that running as actual code. The escape function converts special characters to safe HTML entities.

Popup vs. Background Script vs. Content Script

Chrome extensions can have three types of scripts. Understanding the difference saves you hours of debugging:

Type What It Does When It Runs Can Access
Popup The mini UI when you click the icon Only while popup is open Chrome APIs, extension storage
Background (Service Worker) Handles events behind the scenes When events fire (not constantly) Chrome APIs, extension storage
Content Script Runs inside web pages When matching pages load Page DOM, limited Chrome APIs

Our Tab Saver only needs a popup. No background script (we're not listening for events when the popup is closed) and no content script (we're not modifying web pages). Simpler is better — only add what you actually need.

What AI Gets Wrong

AI is excellent at generating Chrome extension code — but it makes predictable mistakes. Here are the ones that will waste your time if you don't know about them:

1. Manifest V2 vs. V3 Confusion

This is the biggest one. AI models were trained on years of V2 extension tutorials, so they sometimes generate V2 code even when you ask for V3. Watch for these red flags:

// ❌ WRONG — This is Manifest V2 syntax
"background": {
  "scripts": ["background.js"],
  "persistent": false
}

// ✅ RIGHT — Manifest V3 uses service_worker
"background": {
  "service_worker": "background.js"
}
// ❌ WRONG — V2 used "browser_action"
"browser_action": {
  "default_popup": "popup.html"
}

// ✅ RIGHT — V3 uses "action"
"action": {
  "default_popup": "popup.html"
}

Fix: Always explicitly say "Use Manifest V3" in your prompt. If the generated code has browser_action, "manifest_version": 2, or "scripts": ["background.js"], ask AI to convert to V3.

2. Inline JavaScript in HTML

Manifest V3 enforces a strict Content Security Policy. AI frequently generates popup HTML with inline event handlers or inline script blocks:

<!-- ❌ WRONG — Inline onclick will NOT work -->
<button onclick="saveTabs()">Save</button>

<!-- ❌ WRONG — Inline script will NOT work -->
<script>
  function saveTabs() { ... }
</script>

<!-- ✅ RIGHT — External script file -->
<script src="popup.js"></script>

If your extension loads but nothing happens when you click buttons, inline JavaScript is almost certainly the problem.

3. Wrong or Excessive Permissions

AI often requests more permissions than needed. This is bad practice and Chrome will warn users during installation:

// ❌ OVERKILL — Don't request these unless you need them
"permissions": [
  "tabs",
  "storage",
  "activeTab",
  "bookmarks",
  "history",
  "<all_urls>"
]

// ✅ MINIMAL — Only what our extension actually uses
"permissions": [
  "tabs",
  "storage"
]

Rule of thumb: If you can't explain why your extension needs a permission, remove it.

4. Using chrome.* APIs in Content Scripts

Content scripts (the ones that run inside web pages) can only access a tiny subset of Chrome APIs. AI will sometimes try to use chrome.tabs or chrome.storage inside a content script, which silently fails. Those APIs only work in popups and background scripts.

5. Forgetting the Icons

AI generates perfect code but forgets to mention you need icon files. Without at least a 16x16 icon, Chrome shows a generic puzzle piece. You can create simple icons with any image editor, or ask your AI: "Generate a simple 128x128 PNG icon for a tab saver extension — a folder with tabs." Then resize to 16x16 and 48x48.

How to Debug Your Chrome Extension

Debugging Chrome extensions is different from debugging a regular web page, but Chrome gives you excellent tools. Here's your debugging workflow:

Step 1: Check for Load Errors

Go to chrome://extensions. If your extension has a red "Errors" button, click it. The most common errors:

  • "Could not load manifest" — Your JSON is malformed. Check for trailing commas, missing quotes, or typos in key names.
  • "Service worker registration failed" — Your background script file doesn't exist or has a syntax error.
  • "Could not load icon" — The icon file path in manifest.json doesn't match an actual file. You can skip icons initially — just remove the icons and default_icon entries from manifest.json.

Step 2: Inspect the Popup

Right-click your extension icon and select "Inspect popup." This opens Chrome DevTools attached to your popup — console, elements, network, everything. This is where you'll see JavaScript errors.

Pro tip: The popup closes when you click away from it. If you need to keep it open while debugging, open DevTools first (inspect popup), then interact with the popup. DevTools keeps it alive.

Step 3: Check the Console

90% of extension bugs show up as console errors. Common ones:

  • Uncaught TypeError: Cannot read properties of null — Your JavaScript is trying to find an HTML element that doesn't exist. Check your element IDs.
  • Refused to execute inline script — You have inline JavaScript in your HTML. Move it to a separate .js file.
  • chrome.tabs is undefined — You're missing the "tabs" permission in manifest.json, or you're trying to use this API in a content script.

Step 4: The Reload Cycle

After every code change:

  1. Save your files
  2. Go to chrome://extensions
  3. Click the refresh icon (circular arrow) on your extension
  4. Close and reopen the popup
  5. Test the change

You don't need to remove and re-add the extension for code changes — just refresh it. The only time you need to remove/re-add is if you change the permissions in manifest.json.

How to Load Your Extension

This is the moment of truth. Here's exactly how to get your extension running in Chrome:

Step 1: Create Your Project Folder

Create a folder anywhere on your computer (your Desktop works fine). Call it tab-saver. Put these files inside:

tab-saver/
├── manifest.json
├── popup.html
├── popup.js
├── icon16.png    (optional — 16x16 pixels)
├── icon48.png    (optional — 48x48 pixels)
└── icon128.png   (optional — 128x128 pixels)

If you don't have icon files yet, remove the "icons" and "default_icon" sections from manifest.json. Chrome will use a default icon. You can add custom icons later.

Step 2: Enable Developer Mode

  1. Open Chrome and type chrome://extensions in the address bar
  2. In the top-right corner, toggle "Developer mode" ON
  3. You'll see three new buttons appear: "Load unpacked", "Pack extension", and "Update"

Step 3: Load Your Extension

  1. Click "Load unpacked"
  2. Navigate to your tab-saver folder
  3. Select the folder (not a file inside it — the folder itself)
  4. Click "Select Folder" (or "Open" on Mac)

If everything is correct, your extension appears in the list with its name and a toggle switch. If you see a red "Errors" link, click it — Chrome tells you exactly what's wrong.

Step 4: Pin It to Your Toolbar

  1. Click the puzzle piece icon (🧩) in Chrome's toolbar
  2. Find "Tab Saver" in the dropdown
  3. Click the pin icon next to it
  4. Your extension icon now lives permanently in your toolbar

Step 5: Test It

  1. Open a few tabs with different websites
  2. Click your Tab Saver icon
  3. Type a session name like "Work Research"
  4. Click "Save Tabs"
  5. Close those tabs (brave, I know)
  6. Click Tab Saver again and hit "Restore" on your saved session
  7. Watch your tabs come back to life ✨

You just built and installed a real Chrome extension. That's not a tutorial exercise — that's a tool you'll actually use.

Taking It Further

Once your basic Tab Saver works, you can ask AI to add features. Here are prompts that work well:

Enhancement Prompts

Export/Import: "Add an Export button that downloads my saved sessions as a JSON file, and an Import button that loads sessions from a JSON file."

Keyboard shortcut: "Add a keyboard shortcut (Ctrl+Shift+S) that saves the current tabs without opening the popup."

Auto-save: "Add a background service worker that automatically saves all tabs every 30 minutes with a timestamp as the session name."

Search: "Add a search box to the popup that filters saved sessions by name."

Tab groups: "When restoring a session, open all tabs in a Chrome tab group named after the session."

Each of these requires adding just a few lines of code. The keyboard shortcut needs a "commands" entry in manifest.json. The auto-save needs a background service worker. Your AI will handle the implementation — you just need to know what's possible.

What to Learn Next

Building this Chrome extension used several foundational concepts. Strengthen your understanding with these guides:

  • What Is JavaScript? — The language powering your extension's logic. Understand variables, functions, and async/await.
  • What Is JSON? — Your manifest.json uses this format. Learn the rules so you can debug JSON errors yourself.
  • What Is npm? — When your extensions get more complex, you might want to use packages. npm is how you install them.
  • Security Basics for AI Coders — Chrome extensions have access to sensitive data. Understand XSS, permissions, and why Chrome's Content Security Policy exists.
  • Build a To-Do App with AI — If this was your first JavaScript project, the to-do app tutorial teaches DOM manipulation and event handling in more depth.

Frequently Asked Questions

Do I need to know JavaScript to build a Chrome extension?

You don't need to be a JavaScript expert, but understanding the basics helps enormously. Chrome extensions use HTML, CSS, and JavaScript — the same web technologies you've already been working with. If you can read what your AI generates and understand the general flow, you're ready.

What is the difference between Manifest V2 and Manifest V3?

Manifest V3 is Chrome's current extension platform, replacing V2 which was deprecated in 2024. The biggest changes: background pages became service workers (they don't run constantly), remote code execution was blocked for security, and the permissions model was tightened. Always use Manifest V3 — V2 extensions can no longer be uploaded to the Chrome Web Store.

Can I publish my Chrome extension to the Chrome Web Store?

Yes! You need a Google Developer account (one-time $5 fee), a ZIP file of your extension folder, screenshots, and a description. The review process typically takes 1-3 business days. Google checks for policy compliance, malware, and proper permissions usage.

Why does my Chrome extension not update when I change the code?

Chrome caches extension files aggressively. After changing code, go to chrome://extensions, find your extension, and click the circular refresh arrow. For popup changes, close and reopen the popup. For service worker changes, you may need to toggle the extension off and on.

What is the difference between a popup, background script, and content script?

A popup is the small UI window that appears when you click your extension icon. A background script (service worker in V3) runs behind the scenes handling events with no visible UI. A content script gets injected into actual web pages and can read or modify page content. Most extensions use a combination of all three.