Build a Chrome Extension with AI Coding: Complete Walkthrough
Follow along step-by-step as we use AI to build, debug, and publish a Chrome extension from scratch. Includes every prompt and code snippet.
What if you could build a Chrome extension in an afternoon — with zero prior extension experience?
That’s exactly what we’re doing. By the end of this guide, you’ll have a fully functional Tab Bookmarker Pro extension that lets you save, tag, search, and manage bookmarks with a clean popup UI. And you’ll have built it almost entirely through AI prompts.
| What You’ll Build | |
|---|---|
| Extension name | Tab Bookmarker Pro |
| Features | Save current tab, add tags, search bookmarks, export as JSON |
| Tech | Manifest V3, vanilla JS, Chrome Storage API |
| Time | ~2 hours |
| Difficulty | ⭐⭐ Intermediate |
New to vibecoding? Start with our Getting Started guide and prompts that actually work first.
Step 1: Scaffold the Project
Every Chrome extension needs a specific file structure. Let’s have AI create it.
The Prompt:
Create the file structure for a Chrome extension using Manifest V3. The extension is called “Tab Bookmarker Pro” — it saves the current tab as a bookmark with custom tags, stores bookmarks in chrome.storage.local, and has a popup UI for managing them. Give me:
- manifest.json with appropriate permissions (storage, activeTab, tabs)
- popup.html — clean, minimal UI
- popup.js — all the logic
- popup.css — modern dark theme styling
- A 16x16 and 48x48 icon placeholder description
Use Manifest V3 (not V2). No external dependencies.
What AI Generates:
First, manifest.json:
{
"manifest_version": 3,
"name": "Tab Bookmarker Pro",
"version": "1.0.0",
"description": "Save, tag, and search your bookmarks with ease.",
"permissions": ["storage", "activeTab", "tabs"],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
Next, popup.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tab Bookmarker Pro</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<h1>📑 Bookmarker Pro</h1>
<div id="save-section">
<div id="current-tab" class="tab-preview">Loading...</div>
<input type="text" id="tags-input" placeholder="Add tags (comma separated)">
<button id="save-btn" class="primary-btn">Save Bookmark</button>
</div>
<div id="search-section">
<input type="text" id="search-input" placeholder="Search bookmarks...">
</div>
<div id="bookmarks-list"></div>
<div class="footer-actions">
<button id="export-btn" class="secondary-btn">Export JSON</button>
<span id="count">0 bookmarks</span>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
Why this works: The AI gave us a clean separation — HTML structure, CSS styling, JS logic — and used Manifest V3 correctly. Many tutorials still use V2, which Chrome is deprecating.
Step 2: Build the Core Logic
Now let’s get the JavaScript working.
The Prompt:
Write popup.js for the Tab Bookmarker Pro Chrome extension. It should:
- On popup open, get the current tab’s URL and title using chrome.tabs.query
- Display the current tab info in the #current-tab div
- When “Save Bookmark” is clicked, save to chrome.storage.local with: url, title, tags (from comma-separated input), and timestamp
- Load and display all saved bookmarks as a list below
- Each bookmark shows title, URL (truncated), tags as badges, and a delete button
- The search input filters bookmarks by title, URL, or tag in real-time
- Export button downloads all bookmarks as a JSON file
Use async/await. Handle errors gracefully. Show user feedback on save/delete.
What AI Generates:
document.addEventListener('DOMContentLoaded', init);
async function init() {
await displayCurrentTab();
await loadBookmarks();
setupEventListeners();
}
async function displayCurrentTab() {
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true
});
const preview = document.getElementById('current-tab');
preview.innerHTML = `
<strong>${escapeHtml(tab.title)}</strong>
<span class="url">${escapeHtml(truncate(tab.url, 50))}</span>
`;
preview.dataset.url = tab.url;
preview.dataset.title = tab.title;
}
function setupEventListeners() {
document.getElementById('save-btn')
.addEventListener('click', saveBookmark);
document.getElementById('search-input')
.addEventListener('input', handleSearch);
document.getElementById('export-btn')
.addEventListener('click', exportBookmarks);
}
async function saveBookmark() {
const preview = document.getElementById('current-tab');
const tagsInput = document.getElementById('tags-input');
const url = preview.dataset.url;
const title = preview.dataset.title;
const tags = tagsInput.value
.split(',')
.map(t => t.trim().toLowerCase())
.filter(Boolean);
const bookmark = {
id: Date.now().toString(),
url,
title,
tags,
savedAt: new Date().toISOString()
};
const { bookmarks = [] } = await chrome.storage.local.get('bookmarks');
// Check for duplicates
if (bookmarks.some(b => b.url === url)) {
showFeedback('Already bookmarked!', 'warning');
return;
}
bookmarks.unshift(bookmark);
await chrome.storage.local.set({ bookmarks });
tagsInput.value = '';
showFeedback('Saved!', 'success');
await loadBookmarks();
}
async function loadBookmarks(filter = '') {
const { bookmarks = [] } = await chrome.storage.local.get('bookmarks');
const list = document.getElementById('bookmarks-list');
const count = document.getElementById('count');
const filtered = filter
? bookmarks.filter(b =>
b.title.toLowerCase().includes(filter) ||
b.url.toLowerCase().includes(filter) ||
b.tags.some(t => t.includes(filter))
)
: bookmarks;
count.textContent = `${bookmarks.length} bookmark${bookmarks.length !== 1 ? 's' : ''}`;
if (filtered.length === 0) {
list.innerHTML = '<p class="empty">No bookmarks yet. Save your first one!</p>';
return;
}
list.innerHTML = filtered.map(b => `
<div class="bookmark-item" data-id="${b.id}">
<div class="bookmark-info">
<a href="${escapeHtml(b.url)}" target="_blank" class="bookmark-title">
${escapeHtml(b.title)}
</a>
<span class="bookmark-url">${escapeHtml(truncate(b.url, 40))}</span>
<div class="tags">
${b.tags.map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('')}
</div>
</div>
<button class="delete-btn" data-id="${b.id}">✕</button>
</div>
`).join('');
// Attach delete handlers
list.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', () => deleteBookmark(btn.dataset.id));
});
}
async function deleteBookmark(id) {
const { bookmarks = [] } = await chrome.storage.local.get('bookmarks');
const updated = bookmarks.filter(b => b.id !== id);
await chrome.storage.local.set({ bookmarks: updated });
showFeedback('Deleted', 'success');
await loadBookmarks(document.getElementById('search-input').value.toLowerCase());
}
function handleSearch(e) {
loadBookmarks(e.target.value.toLowerCase());
}
async function exportBookmarks() {
const { bookmarks = [] } = await chrome.storage.local.get('bookmarks');
const blob = new Blob([JSON.stringify(bookmarks, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `bookmarks-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
}
function showFeedback(message, type) {
const existing = document.querySelector('.feedback');
if (existing) existing.remove();
const div = document.createElement('div');
div.className = `feedback ${type}`;
div.textContent = message;
document.querySelector('.container').prepend(div);
setTimeout(() => div.remove(), 2000);
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function truncate(str, len) {
return str.length > len ? str.substring(0, len) + '...' : str;
}
This is solid, production-ready code. The AI handled all the edge cases we asked for — duplicate checking, HTML escaping, search filtering, and graceful error messages.
Step 3: Style It
The Prompt:
Write popup.css for the Tab Bookmarker Pro Chrome extension. Dark theme, modern look. The popup should be 360px wide and max 500px tall with scroll. Style the bookmark items as cards. Tags should be small colored badges. Use smooth transitions. Make it feel like a real product, not a tutorial project.
What AI Generates:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 360px;
max-height: 500px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #1a1a2e;
color: #e0e0e0;
overflow-y: auto;
}
.container {
padding: 16px;
}
h1 {
font-size: 18px;
margin-bottom: 12px;
color: #fff;
}
.tab-preview {
background: #16213e;
border: 1px solid #0f3460;
border-radius: 8px;
padding: 10px;
margin-bottom: 8px;
font-size: 13px;
}
.tab-preview .url {
display: block;
color: #888;
font-size: 11px;
margin-top: 4px;
}
input[type="text"] {
width: 100%;
padding: 8px 12px;
border: 1px solid #0f3460;
border-radius: 6px;
background: #16213e;
color: #e0e0e0;
font-size: 13px;
margin-bottom: 8px;
outline: none;
transition: border-color 0.2s;
}
input[type="text"]:focus {
border-color: #e94560;
}
.primary-btn {
width: 100%;
padding: 10px;
background: #e94560;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
margin-bottom: 12px;
}
.primary-btn:hover {
background: #c73e54;
}
#search-section {
margin-bottom: 12px;
}
.bookmark-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
background: #16213e;
border: 1px solid #0f3460;
border-radius: 8px;
padding: 10px;
margin-bottom: 6px;
transition: transform 0.15s, border-color 0.2s;
}
.bookmark-item:hover {
border-color: #e94560;
transform: translateX(2px);
}
.bookmark-title {
color: #e94560;
text-decoration: none;
font-size: 13px;
font-weight: 500;
}
.bookmark-title:hover {
text-decoration: underline;
}
.bookmark-url {
display: block;
color: #666;
font-size: 11px;
margin-top: 2px;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 6px;
}
.tag {
background: #0f3460;
color: #7ec8e3;
padding: 2px 8px;
border-radius: 12px;
font-size: 10px;
}
.delete-btn {
background: none;
border: none;
color: #666;
cursor: pointer;
font-size: 16px;
padding: 4px;
transition: color 0.2s;
}
.delete-btn:hover {
color: #e94560;
}
.footer-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #0f3460;
}
.secondary-btn {
padding: 6px 12px;
background: #0f3460;
color: #7ec8e3;
border: none;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: background 0.2s;
}
.secondary-btn:hover {
background: #1a4a7a;
}
#count {
font-size: 12px;
color: #666;
}
.feedback {
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
margin-bottom: 8px;
animation: fadeIn 0.2s ease;
}
.feedback.success {
background: #0a3d2a;
color: #4ade80;
}
.feedback.warning {
background: #3d2a0a;
color: #fbbf24;
}
.empty {
text-align: center;
color: #666;
font-size: 13px;
padding: 20px 0;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
Step 4: Create Icons
You need icons. Easiest approach:
The Prompt:
Generate a simple SVG icon for a bookmark/tab Chrome extension. Use a bookmark shape with a subtle gradient. I need it at 16x16, 48x48, and 128x128.
AI will give you SVG code. But for the Chrome Web Store, you need PNGs. Quick workaround:
- Paste the SVG into any browser
- Screenshot at each size
- Or use a free tool like RealFaviconGenerator
For development, create a simple icons/ folder with placeholder PNGs. You can even draw a colored square in any image editor — it just needs to exist.
Step 5: Load and Test
Here’s where it gets real. Time to load your extension into Chrome.
- Open
chrome://extensions/ - Enable Developer mode (toggle in top right)
- Click Load unpacked
- Select your project folder
The extension should appear in your toolbar.
Click the icon. You should see the popup with the current tab’s info.
What Went Wrong: Debugging
Almost nobody gets a clean first load. Here’s what typically breaks:
Problem 1: “Uncaught TypeError: Cannot read properties of undefined”
This happens when chrome.tabs.query returns before the popup DOM is ready.
Fix prompt:
The chrome.tabs.query call in my popup.js is returning undefined. The popup opens but shows “Loading…” forever. I’m using Manifest V3 with the activeTab permission. How do I fix this?
The fix is usually wrapping in DOMContentLoaded (which we already did) and making sure the async function has proper error handling:
async function displayCurrentTab() {
try {
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true
});
if (!tab) {
document.getElementById('current-tab').textContent = 'No active tab found';
return;
}
// ... rest of the code
} catch (err) {
console.error('Failed to get tab:', err);
}
}
Problem 2: CSP (Content Security Policy) Errors
If you accidentally add inline scripts in your HTML, Manifest V3 will block them. The error looks like:
Refused to execute inline script because it violates the following
Content Security Policy directive...
The fix: All JavaScript must be in separate .js files. No onclick="" attributes, no <script> tags with inline code. This is why we have a separate popup.js.
Problem 3: Storage Not Persisting
If bookmarks disappear when you close and reopen the popup, make sure you’re using chrome.storage.local, not localStorage. They’re different APIs:
localStorage— scoped to the popup page, cleared when popup closeschrome.storage.local— persists across popup sessions ✅
Step 6: Add Polish
Let’s add one more feature — keyboard shortcuts.
The Prompt:
Add to popup.js: pressing Enter in the tags input should trigger save. Pressing Escape should close the popup. Also add a keyboard shortcut in manifest.json: Ctrl+Shift+B to open the extension.
Add to manifest.json:
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Ctrl+Shift+B",
"mac": "Command+Shift+B"
},
"description": "Open Tab Bookmarker Pro"
}
}
Add to popup.js:
document.getElementById('tags-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') saveBookmark();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') window.close();
});
Step 7: Publish to the Chrome Web Store
Ready to ship? Here’s the process:
- Create a ZIP of your extension folder (not the parent folder — the one containing
manifest.json) - Go to the Chrome Web Store Developer Dashboard
- Pay the one-time $5 registration fee
- Click New Item → Upload your ZIP
- Fill in:
- Description (ask AI to write one based on your features)
- Screenshots (take them from the popup)
- Category: Productivity
- Language: English
- Submit for review
Prompt for the store description:
Write a Chrome Web Store listing description for “Tab Bookmarker Pro” — an extension that saves tabs as bookmarks with custom tags, search, and JSON export. Keep it under 400 words. Include a features list and a short pitch paragraph.
Review typically takes 1-3 business days. Common rejection reasons:
- Missing privacy policy (add a simple one hosted on a free site)
- Requesting more permissions than needed
- Missing or inadequate icon
The Full File Structure
tab-bookmarker-pro/
├── manifest.json
├── popup.html
├── popup.js
├── popup.css
└── icons/
├── icon16.png
├── icon48.png
└── icon128.png
That’s it. Five files (plus icons) for a fully functional, publishable Chrome extension.
Next Steps
You just built a Chrome extension with AI. Here’s where to go from here:
- Add a background service worker to save tabs automatically when you close them
- Add sync storage (
chrome.storage.sync) to sync bookmarks across devices - Build a content script that highlights bookmarked pages with a subtle indicator
- Add import functionality so users can restore from exported JSON
Check out our other guides:
- Prompts That Actually Work — refine your AI prompting skills
- 7 Mistakes Every New Vibecoder Makes — avoid the common traps
- 10 Perfect First Projects — find your next build
The whole point of vibecoding is this: you don’t need to memorize Chrome extension APIs. You need to know what you want to build, and how to describe it clearly to AI. The API knowledge comes from the AI. The vision comes from you.
Now go build something. 🚀