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:
"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 openpopup.htmlas 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: 350pxin CSS for a clean look. - No inline scripts: In Manifest V3, you cannot put JavaScript directly in your HTML with inline
<script>tags oronclick="..."attributes. You must use a separate.jsfile. 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 usechrome.storage.syncinstead.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
iconsanddefault_iconentries 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:
- Save your files
- Go to
chrome://extensions - Click the refresh icon (circular arrow) on your extension
- Close and reopen the popup
- 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
- Open Chrome and type
chrome://extensionsin the address bar - In the top-right corner, toggle "Developer mode" ON
- You'll see three new buttons appear: "Load unpacked", "Pack extension", and "Update"
Step 3: Load Your Extension
- Click "Load unpacked"
- Navigate to your
tab-saverfolder - Select the folder (not a file inside it — the folder itself)
- 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
- Click the puzzle piece icon (🧩) in Chrome's toolbar
- Find "Tab Saver" in the dropdown
- Click the pin icon next to it
- Your extension icon now lives permanently in your toolbar
Step 5: Test It
- Open a few tabs with different websites
- Click your Tab Saver icon
- Type a session name like "Work Research"
- Click "Save Tabs"
- Close those tabs (brave, I know)
- Click Tab Saver again and hit "Restore" on your saved session
- 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:
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.