Hey there! So you wanna build a Chrome extension? Awesome! It’s way easier than you think. Seriously, you can have a basic one running in like 5 minutes. Let me walk you through everything you need to know.
Just build a leads data extractor for myself and a client! Not my first Chrome Extension, but the first that used Manifest V3 and all the new features :)
What Even Is a Chrome Extension?
Basically, it’s just HTML, CSS, and JavaScript that adds cool features to your browser. That’s it! No fancy frameworks required (unless you want them). Think of it like adding superpowers to Chrome.
The Big Picture: What You Need to Know
Before we dive in, here’s the main stuff:
- Manifest V3 is the current version (as of 2025). Don’t use V2 tutorials – they’re outdated!
- Your extension lives in a folder with all its files
- The
manifest.jsonfile is like the passport – it tells Chrome everything about your extension - You can have popups, options pages, content scripts, and background workers
- Testing is easy – just load it unpacked in Chrome
The Basic Structure
Here’s what a typical extension folder looks like:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
my-awesome-extension/ │ ├── manifest.json # The heart - tells Chrome about your extension ├── background.js # Background service worker (optional) ├── popup.html # What shows when you click the icon ├── popup.js # Logic for the popup ├── popup.css # Styles for the popup ├── options.html # Settings page (optional but cool) ├── options.js # Logic for options page ├── content.js # Runs on actual web pages (optional) └── images/ ├── icon16.png # Extension icon (16x16) ├── icon48.png # Extension icon (48x48) └── icon128.png # Extension icon (128x128) |
Step 1: The manifest.json File
This is THE most important file. Here’s a full example with all the bells and whistles:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
{ "manifest_version": 3, "name": "My Awesome Extension", "version": "1.0.0", "description": "Does cool stuff in your browser!", "icons": { "16": "images/icon16.png", "48": "images/icon48.png", "128": "images/icon128.png" }, "action": { "default_popup": "popup.html", "default_icon": { "16": "images/icon16.png", "48": "images/icon48.png", "128": "images/icon128.png" }, "default_title": "Click me!" }, "background": { "service_worker": "background.js" }, "content_scripts": [ { "matches": ["<all_urls>"], "js": ["content.js"], "css": ["content.css"] } ], "options_ui": { "page": "options.html", "open_in_tab": false }, "permissions": [ "storage", "activeTab" ], "host_permissions": [ "https://*/*", "http://*/*" ] }</all_urls> |
Breaking Down the Manifest:
manifest_version: Always use 3 – it’s the current standard!
action: This is what happens when someone clicks your extension icon. In V3, this replaced browser_action.
background: This is your service worker – it runs in the background and handles events. Unlike V2, these don’t run 24/7 – they wake up when needed then go to sleep.
content_scripts: These run ON actual web pages and can modify them. Super powerful!
options_ui: This is where your settings page goes. More on this in a sec!
permissions: What APIs you need access to (like storage, tabs, etc.)
host_permissions: Which websites your extension can access
Step 2: Creating the Popup
The popup is what shows up when someone clicks your extension icon. Here’s a simple example:
popup.html
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My Extension</title> <link rel="stylesheet" href="popup.css"> </head> <body> <div class="container"> <h1>Hello!</h1> <p>Click count: <span id="count">0</span></p> <button id="myButton">Click Me!</button> <button id="goToOptions">Settings</button> </div> <script src="popup.js"></script> </body> </html> |
popup.css
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
body { width: 300px; padding: 20px; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } .container { text-align: center; } h1 { margin-top: 0; font-size: 24px; } button { margin: 10px 5px; padding: 10px 20px; border: none; border-radius: 5px; background: white; color: #667eea; font-weight: bold; cursor: pointer; transition: transform 0.2s; } button:hover { transform: scale(1.05); } #count { font-weight: bold; font-size: 20px; } |
popup.js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
document.addEventListener('DOMContentLoaded', function() { const countSpan = document.getElementById('count'); const myButton = document.getElementById('myButton'); const goToOptions = document.getElementById('goToOptions'); // Load saved count from storage chrome.storage.sync.get(['clickCount'], function(result) { countSpan.textContent = result.clickCount || 0; }); // Button click handler myButton.addEventListener('click', function() { chrome.storage.sync.get(['clickCount'], function(result) { let newCount = (result.clickCount || 0) + 1; chrome.storage.sync.set({ clickCount: newCount }); countSpan.textContent = newCount; // Send message to background script chrome.runtime.sendMessage({ action: 'buttonClicked', count: newCount }); // Send message to content script on current tab chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { chrome.tabs.sendMessage(tabs[0].id, { action: 'updateCount', count: newCount }); }); }); }); // Open options page goToOptions.addEventListener('click', function() { chrome.runtime.openOptionsPage(); }); }); |
Step 3: The Options Page (Settings Page)
This is super important! It lets users customize your extension. Here’s how to set it up:
options.html
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Extension Settings</title> <style> body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 600px; margin: 50px auto; padding: 30px; background: #f5f5f5; } .settings-container { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } h1 { color: #667eea; margin-top: 0; } .setting-group { margin: 25px 0; padding: 20px; background: #f9f9f9; border-radius: 5px; } label { display: block; margin-bottom: 10px; font-weight: bold; color: #333; } input[type="text"], input[type="number"], select { width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px; font-size: 14px; box-sizing: border-box; } input[type="checkbox"] { width: 20px; height: 20px; margin-right: 10px; cursor: pointer; } .checkbox-label { display: flex; align-items: center; cursor: pointer; } input[type="color"] { width: 60px; height: 40px; border: none; border-radius: 5px; cursor: pointer; } button { background: #667eea; color: white; border: none; padding: 12px 30px; border-radius: 5px; font-size: 16px; font-weight: bold; cursor: pointer; margin-right: 10px; transition: background 0.3s; } button:hover { background: #5568d3; } .reset-btn { background: #e74c3c; } .reset-btn:hover { background: #c0392b; } .status { display: none; padding: 10px; margin-top: 20px; border-radius: 5px; text-align: center; font-weight: bold; } .status.show { display: block; } .status.success { background: #2ecc71; color: white; } .status.error { background: #e74c3c; color: white; } </style> </head> <body> <div class="settings-container"> <h1>Extension Settings</h1> <div class="setting-group"> <label for="username">Your Name:</label> <input type="text" id="username" placeholder="Enter your name"> </div> <div class="setting-group"> <label for="theme">Theme Color:</label> <input type="color" id="theme" value="#667eea"> </div> <div class="setting-group"> <label for="interval">Update Interval (seconds):</label> <input type="number" id="interval" value="60" min="1" max="3600"> </div> <div class="setting-group"> <label for="mode">Operation Mode:</label> <select id="mode"> <option value="basic">Basic</option> <option value="advanced">Advanced</option> <option value="expert">Expert</option> </select> </div> <div class="setting-group"> <label class="checkbox-label"> <input type="checkbox" id="notifications"> <span>Enable Notifications</span> </label> <label class="checkbox-label"> <input type="checkbox" id="autostart"> <span>Auto-start on Browser Launch</span> </label> <label class="checkbox-label"> <input type="checkbox" id="darkmode"> <span>Dark Mode</span> </label> </div> <button id="save">Save Settings</button> <button id="reset" class="reset-btn">Reset to Default</button> <div id="status" class="status"></div> </div> <script src="options.js"></script> </body> </html> |
options.js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
// Default settings const defaultSettings = { username: '', theme: '#667eea', interval: 60, mode: 'basic', notifications: true, autostart: false, darkmode: false }; // Load saved settings when page loads document.addEventListener('DOMContentLoaded', restoreOptions); // Save button click handler document.getElementById('save').addEventListener('click', saveOptions); // Reset button click handler document.getElementById('reset').addEventListener('click', resetOptions); function saveOptions() { const settings = { username: document.getElementById('username').value, theme: document.getElementById('theme').value, interval: parseInt(document.getElementById('interval').value), mode: document.getElementById('mode').value, notifications: document.getElementById('notifications').checked, autostart: document.getElementById('autostart').checked, darkmode: document.getElementById('darkmode').checked }; // Save to chrome.storage chrome.storage.sync.set(settings, function() { // Show success message showStatus('Settings saved successfully!', 'success'); // Notify background script about settings change chrome.runtime.sendMessage({ action: 'settingsUpdated', settings: settings }); }); } function restoreOptions() { chrome.storage.sync.get(defaultSettings, function(items) { document.getElementById('username').value = items.username; document.getElementById('theme').value = items.theme; document.getElementById('interval').value = items.interval; document.getElementById('mode').value = items.mode; document.getElementById('notifications').checked = items.notifications; document.getElementById('autostart').checked = items.autostart; document.getElementById('darkmode').checked = items.darkmode; }); } function resetOptions() { if (confirm('Are you sure you want to reset all settings to default?')) { chrome.storage.sync.set(defaultSettings, function() { restoreOptions(); showStatus('Settings reset to default!', 'success'); }); } } function showStatus(message, type) { const status = document.getElementById('status'); status.textContent = message; status.className = `status show ${type}`; setTimeout(function() { status.classList.remove('show'); }, 3000); } |
Important Options Page Notes:
Two ways to show options:
- Embedded (inside chrome://extensions): Set
"open_in_tab": false - Full tab (opens in a new tab): Set
"open_in_tab": true
Most people use embedded because it’s cleaner, but if you have lots of settings, use a full tab!
Opening the options page:
- Users can right-click your icon and select “Options”
- Go to chrome://extensions, find your extension, click “Details”, then “Extension options”
- You can open it programmatically:
chrome.runtime.openOptionsPage()
Step 4: Background Service Worker
This runs in the background and handles events. It’s like the brain of your extension.
background.js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
// This runs when the extension is installed or updated chrome.runtime.onInstalled.addListener((details) => { if (details.reason === 'install') { console.log('Extension installed!'); // Set default settings chrome.storage.sync.set({ clickCount: 0, username: '', theme: '#667eea' }); } else if (details.reason === 'update') { console.log('Extension updated!'); } }); // Listen for messages from popup or content scripts chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { console.log('Message received:', request); if (request.action === 'buttonClicked') { console.log('Button clicked! Count:', request.count); // Do something with this info } if (request.action === 'settingsUpdated') { console.log('Settings updated:', request.settings); // React to settings changes } // If you need to send a response back sendResponse({ status: 'Message received!' }); // Return true if you're sending response asynchronously return true; }); // Listen for tab updates chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (changeInfo.status === 'complete') { console.log('Page loaded:', tab.url); } }); // Create a context menu item chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.create({ id: 'myContextMenu', title: 'Do Something Cool', contexts: ['page', 'selection'] }); }); // Handle context menu clicks chrome.contextMenus.onClicked.addListener((info, tab) => { if (info.menuItemId === 'myContextMenu') { console.log('Context menu clicked!'); // Do something cool here } }); // Set up periodic tasks (alarms) chrome.alarms.create('myAlarm', { periodInMinutes: 1 }); chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === 'myAlarm') { console.log('Alarm triggered!'); // Do periodic tasks } }); |
Step 5: Content Scripts
These run ON web pages and can modify them. Super powerful!
content.js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
console.log('Content script loaded!'); // Listen for messages from popup or background chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === 'updateCount') { console.log('New count:', request.count); // Maybe show a notification on the page showNotification(`Count updated to ${request.count}!`); } if (request.action === 'changeColor') { document.body.style.backgroundColor = getRandomColor(); sendResponse({ status: 'Color changed!' }); } return true; }); // Example: Add a floating button to every page function addFloatingButton() { const button = document.createElement('button'); button.textContent = 'Launch'; button.style.cssText = ` position: fixed; bottom: 20px; right: 20px; width: 60px; height: 60px; border-radius: 50%; border: none; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; font-size: 24px; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 10000; transition: transform 0.3s; `; button.addEventListener('mouseenter', () => { button.style.transform = 'scale(1.1)'; }); button.addEventListener('mouseleave', () => { button.style.transform = 'scale(1)'; }); button.addEventListener('click', () => { alert('Button clicked from content script!'); }); document.body.appendChild(button); } // Add button when page loads if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', addFloatingButton); } else { addFloatingButton(); } function showNotification(message) { const notification = document.createElement('div'); notification.textContent = message; notification.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 15px 25px; background: #2ecc71; color: white; border-radius: 5px; box-shadow: 0 4px 12px rgba(0,0,0,0.2); z-index: 10001; font-family: Arial, sans-serif; animation: slideIn 0.3s ease-out; `; document.body.appendChild(notification); setTimeout(() => { notification.style.animation = 'slideOut 0.3s ease-out'; setTimeout(() => notification.remove(), 300); }, 3000); } function getRandomColor() { const letters = '0123456789ABCDEF'; let color = '#'; for (let i = 0; i < 6; i++) { color += letters[Math.floor(Math.random() * 16)]; } return color; } |
Step 6: Loading Your Extension
Okay, you’ve built it – now let’s test it!
- Open Chrome and go to
chrome://extensions - Toggle “Developer mode” (top right corner)
- Click “Load unpacked”
- Select your extension folder (the one with manifest.json)
- Done! Your extension should appear!
Testing Tips:
- For popup changes: Just close and reopen the popup
- For content script changes: Reload the webpage
- For background script changes: Click the reload icon on your extension card
- Debugging popup: Right-click the extension icon and select “Inspect popup”
- Debugging background: Click “service worker” link in chrome://extensions
- Debugging content scripts: Use regular DevTools on the page (F12)
Common Permissions You’ll Need
Add these to your manifest.json as needed:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
"permissions": [ "storage", // Save data "tabs", // Access to tab info "activeTab", // Access current tab only (better privacy) "notifications", // Show notifications "contextMenus", // Add right-click menu items "alarms", // Scheduled tasks "clipboardWrite", // Copy to clipboard "cookies", // Access cookies "downloads", // Download files "history", // Access browsing history "bookmarks" // Access bookmarks ] |
Pro tip: Only request permissions you actually need! Users don’t trust extensions with too many permissions.
All Window Types Explained
Your extension can have multiple UI surfaces:
1. Popup (Most Common)
Shows when user clicks icon. Use for quick actions.
|
1 2 3 4 5 |
"action": { "default_popup": "popup.html" } |
2. Options Page (Settings)
Full settings page. Use for configuration.
|
1 2 3 4 5 6 |
"options_ui": { "page": "options.html", "open_in_tab": false // or true for full tab } |
3. Side Panel (NEW in V3!)
Persistent sidebar in Chrome. Great for tools that need to stay open.
|
1 2 3 4 5 |
"side_panel": { "default_path": "sidepanel.html" } |
4. DevTools Panel
Adds a tab to Chrome DevTools. For developer tools.
|
1 2 3 |
"devtools_page": "devtools.html" |
5. Override Pages
Replace Chrome’s default pages:
|
1 2 3 4 5 6 7 |
"chrome_url_overrides": { "newtab": "newtab.html", // New tab page "bookmarks": "bookmarks.html", // Bookmarks page "history": "history.html" // History page } |
Communication Between Parts
Extensions have different parts that need to talk to each other:
From Popup to Background:
|
1 2 3 4 5 |
chrome.runtime.sendMessage({ action: 'doSomething' }, (response) => { console.log(response); }); |
From Background to Content Script:
|
1 2 3 4 5 |
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { chrome.tabs.sendMessage(tabs[0].id, { action: 'doSomething' }); }); |
From Content Script to Background:
|
1 2 3 |
chrome.runtime.sendMessage({ action: 'doSomething' }); |
Listening for Messages (any part):
|
1 2 3 4 5 6 7 8 9 |
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === 'doSomething') { // Do the thing sendResponse({ status: 'Done!' }); } return true; // Important for async responses! }); |
Using Chrome Storage
Way better than localStorage! It syncs across devices.
Save Data:
|
1 2 3 4 5 |
chrome.storage.sync.set({ key: 'value' }, () => { console.log('Saved!'); }); |
Load Data:
|
1 2 3 4 5 |
chrome.storage.sync.get(['key'], (result) => { console.log('Value:', result.key); }); |
Load with Default:
|
1 2 3 4 5 |
chrome.storage.sync.get({ key: 'defaultValue' }, (result) => { console.log('Value:', result.key); }); |
Listen for Changes:
|
1 2 3 4 5 6 7 |
chrome.storage.onChanged.addListener((changes, namespace) => { for (let key in changes) { console.log(`${key} changed from ${changes[key].oldValue} to ${changes[key].newValue}`); } }); |
Publishing Your Extension
Ready to share with the world?
- Go to Chrome Web Store Developer Dashboard
- Pay the $5 one-time developer fee
- Zip your extension folder (not the parent folder!)
- Upload the zip file
- Fill out the listing (description, screenshots, etc.)
- Submit for review
- Wait (usually 1-3 days)
- Get published!
Before Publishing:
- Test thoroughly
- Create nice icons (16×16, 48×48, 128×128)
- Write a clear description
- Take screenshots
- Only request necessary permissions
- Remove any console.logs
- Add a privacy policy if you collect data
Troubleshooting Common Issues
Extension Won’t Load?
- Check manifest.json for syntax errors (use a JSON validator)
- Make sure all referenced files exist
- Check the error message in chrome://extensions
Popup Won’t Show?
- Make sure default_popup is spelled correctly
- Check that popup.html exists
- Open DevTools for the popup (right-click icon and select Inspect)
Content Script Not Running?
- Check if matches pattern is correct
- Make sure you reloaded the page after loading the extension
- Check the console on the webpage
Storage Not Working?
- Did you add “storage” permission?
- Are you using chrome.storage.sync (not localStorage)?
- Check quota limits (sync: 100KB, local: 5MB)
Pro Tips
- Use chrome.storage.sync for settings – it syncs across devices!
- Keep popup code minimal – it reloads every time
- Service workers can’t use DOM APIs – they’re not on a page
- Always handle async properly with promises or callbacks
- Test on different screen sizes – popups can look weird
- Use semantic versioning (1.0.0, 1.0.1, 1.1.0, 2.0.0)
- Comment your code – you’ll thank yourself later
- Check the official docs when stuck
- Join the Chrome Extensions Google Group for help
- Start simple – don’t try to build everything at once!
Example: Complete Minimal Extension
Want to see it all together? Here’s a super simple but complete extension:
File structure:
|
1 2 3 4 5 6 7 |
simple-extension/ ├── manifest.json ├── popup.html ├── popup.js └── icon.png (any size, we'll scale it) |
manifest.json:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "manifest_version": 3, "name": "Simple Counter", "version": "1.0.0", "description": "Counts clicks!", "action": { "default_popup": "popup.html" }, "permissions": ["storage"] } |
popup.html:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!DOCTYPE html> <html> <head> <style> body { width: 200px; padding: 20px; text-align: center; } button { padding: 10px 20px; cursor: pointer; } </style> </head> <body> <h3>Clicks: <span id="count">0</span></h3> <button id="btn">Click Me!</button> <script src="popup.js"></script> </body> </html> |
popup.js:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
document.addEventListener('DOMContentLoaded', () => { const btn = document.getElementById('btn'); const countSpan = document.getElementById('count'); chrome.storage.sync.get(['count'], (result) => { countSpan.textContent = result.count || 0; }); btn.addEventListener('click', () => { chrome.storage.sync.get(['count'], (result) => { const newCount = (result.count || 0) + 1; chrome.storage.sync.set({ count: newCount }); countSpan.textContent = newCount; }); }); }); |
That’s it! Load it and you have a working extension!
Resources & Next Steps
Official Docs:
Thoughts
Building Chrome extensions is honestly one of the most fun things you can do as a developer. You can create something useful in an afternoon, and millions of people could potentially use it!
Start small, experiment, break things, and most importantly – have fun with it! The Chrome Extensions API is super powerful and well-documented. Don’t be afraid to dive into the docs when you need to do something specific.
Now go build something awesome!
FAQ
What is a Chrome extension?
A Chrome extension is a small software program that customizes and enhances your browsing experience. Extensions are built using standard web technologies like HTML, CSS, and JavaScript, making them accessible to web developers. They can add new features to the browser, modify existing functionality, or interact with web pages to improve productivity and user experience.
What is Manifest V3 and why is it important?
Manifest V3 is the latest version of the Chrome extensions platform, mandatory for all new extensions as of 2025. It focuses on improving security, privacy, and performance. Key changes include replacing background pages with service workers, removing remotely hosted code execution, and introducing the declarativeNetRequest API. Extensions must comply with Manifest V3 to be published or remain on the Chrome Web Store.
What files do I need to create a basic Chrome extension?
At minimum, you need a manifest.json file (required) and a 128×128 pixel icon. The manifest.json is a metadata file that describes your extension’s name, version, permissions, and functionality. Additional files typically include JavaScript files for background scripts or content scripts, HTML files for popups or options pages, and CSS files for styling.
How do I load and test my extension locally?
To test your extension locally: 1) Open Chrome and navigate to chrome://extensions/, 2) Enable Developer Mode using the toggle in the top-right corner, 3) Click “Load unpacked” and select your extension’s folder containing the manifest.json file. Your extension will appear in the toolbar and you can test it immediately. Use the “Reload” button to apply changes after editing your code.
What’s the difference between background scripts, content scripts, and popup scripts?
Background scripts (service workers in Manifest V3) run in the background and handle events like browser startup or button clicks. Content scripts run in the context of web pages and can read or modify page content using the DOM. Popup scripts control the UI shown when users click the extension icon in the toolbar. Each runs in a separate context with different capabilities and access levels.
How do I debug my Chrome extension?
Debugging varies by component: For service workers, click “Inspect views” on the chrome://extensions page. For popups, right-click the extension icon and select “Inspect popup”. For content scripts, open DevTools on the web page where the script runs and select your extension from the dropdown menu. Check the “Errors” button on the extensions page for runtime errors. Use console.log() statements and breakpoints for detailed debugging.
What permissions should I request for my extension?
Follow the principle of least privilege – only request permissions absolutely necessary for your extension’s functionality. Use “activeTab” for temporary access to the current tab instead of broad host permissions. Consider optional permissions for features users can enable at runtime. Excessive permissions trigger warning messages that may deter users from installing your extension and could lead to rejection from the Chrome Web Store.
How do I publish my extension to the Chrome Web Store?
To publish: 1) Create a developer account ($5 one-time fee), 2) Zip your extension folder with manifest.json in the root, 3) Go to the Chrome Developer Dashboard, 4) Upload your ZIP file, 5) Complete the store listing with description, screenshots (required), icons, and privacy policy, 6) Submit for review. Review typically takes 1-3 days. You can publish as Public (everyone), Unlisted (link only), or Private (domain-restricted).
What are common reasons for Chrome Web Store rejection?
Common rejection reasons include: requesting unnecessary permissions, using misleading descriptions or screenshots, non-compliance with Manifest V3 requirements, including remotely hosted code, inadequate privacy policy, violating the single-purpose policy, poor quality or broken functionality, and suspicious or malicious behavior. Always review Chrome Web Store Developer Program Policies before submission.
Can I use external libraries in my Chrome extension?
Yes, but all code must be included in your extension package. Manifest V3 prohibits remotely hosted code for security reasons. You must bundle libraries locally using importScripts() in service workers or script tags in HTML pages. Some libraries require modification if they rely on browser APIs not available in service workers, like the window object. Consider using the offscreen API for DOM-dependent libraries.
How do service workers differ from background pages in Manifest V2?
Service workers in Manifest V3 are event-driven and terminate when idle, unlike persistent background pages in V2. They don’t have access to the DOM or window object. Variables don’t persist between activations, so use chrome.storage API for data persistence. Service workers improve performance and battery life but require different coding patterns, particularly for timing-sensitive operations and state management.
What Chrome APIs are available for extension development?
Chrome provides extensive APIs including: chrome.storage for data persistence, chrome.tabs for tab management, chrome.scripting for code injection, chrome.contextMenus for context menu items, chrome.notifications for system notifications, chrome.downloads for file downloads, chrome.bookmarks for bookmark management, chrome.history for browsing history, and many more. Each API requires specific permissions declared in the manifest file. Check the Chrome Extensions API reference for complete documentation.
How do I handle cross-origin requests in my extension?
Extensions must declare host permissions in the manifest for any external URLs they need to access via fetch() or XMLHttpRequest(). Add the URL patterns to the “host_permissions” field. For example: “host_permissions”: [“https://api.example.com/*”]. Always use HTTPS for security. Requests are subject to the extension’s Content Security Policy. Consider using optional_host_permissions for runtime user consent.
What is the activeTab permission and when should I use it?
The activeTab permission grants temporary access to the currently active tab when the user invokes your extension (e.g., clicking the toolbar icon). It doesn’t trigger permission warnings and is automatically revoked when the user navigates away or closes the tab. Use activeTab instead of broad host permissions whenever possible to minimize security warnings and improve user trust.
How do I migrate my Manifest V2 extension to V3?
Key migration steps: 1) Update manifest_version to 3, 2) Replace background.page/scripts with background.service_worker, 3) Move host permissions from permissions to host_permissions array, 4) Update chrome.tabs.executeScript to chrome.scripting.executeScript, 5) Replace webRequest with declarativeNetRequest where applicable, 6) Update web_accessible_resources to the new object format, 7) Remove remotely hosted code, 8) Test thoroughly as service workers behave differently than background pages.
Can I monetize my Chrome extension?
Yes, several monetization strategies exist: freemium models with premium features, subscription services, in-extension purchases, affiliate marketing, sponsorships, or licensing to businesses. However, you must comply with Chrome Web Store policies – extensions cannot include misleading ads, inject unauthorized ads into web pages, or harvest user data for sale. Always be transparent about how your extension makes money and respect user privacy.
What are best practices for extension security?
Security best practices include: request minimal permissions, implement Content Security Policy (CSP), validate all user inputs, use HTTPS for network requests, avoid eval() and inline scripts, sanitize data before injecting into pages, keep dependencies updated, use secure authentication methods, encrypt sensitive data, regularly audit code for vulnerabilities, and enable two-factor authentication on your developer account. Never trust external data sources.
How do I update my published extension?
To update: 1) Increment the version number in manifest.json, 2) Make your changes, 3) Create a new ZIP file, 4) Upload to the Chrome Developer Dashboard (same item ID), 5) Submit for review. Updates go through the same review process. You can stage updates for review but defer publishing, use gradual rollout to release to a percentage of users first, or publish immediately. Users receive updates automatically within hours once approved.
What’s the difference between content_scripts and scripting.executeScript?
content_scripts in the manifest automatically inject into matching pages on load. They’re declared statically and run every time. chrome.scripting.executeScript injects code programmatically at runtime, giving you control over when and where to inject. Use content_scripts for consistent page modifications and executeScript for conditional, user-triggered, or dynamic injections. executeScript requires the scripting permission.
How do I communicate between different parts of my extension?
Use message passing APIs: chrome.runtime.sendMessage() for one-time messages, chrome.runtime.connect() for long-lived connections between extension components. For content script to service worker communication, use chrome.runtime.sendMessage(). For tab-specific messaging, use chrome.tabs.sendMessage(). All messages are JSON-serializable. Handle messages with chrome.runtime.onMessage.addListener(). Consider using chrome.storage for shared state.
Here’s a basic manifest.json example for Manifest V3:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
{ "manifest_version": 3, "name": "My Extension", "version": "1.0.0", "description": "A helpful Chrome extension", "icons": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" }, "action": { "default_popup": "popup.html", "default_icon": "icons/icon48.png" }, "background": { "service_worker": "background.js" }, "permissions": [ "storage", "activeTab" ], "host_permissions": [ "https://api.example.com/*" ], "content_scripts": [{ "matches": ["https://www.example.com/*"], "js": ["content.js"] }] } |
How do I implement chrome.storage for data persistence?
chrome.storage provides three areas: storage.local (local storage), storage.sync (synced across devices), and storage.session (session-based). Basic usage:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Save data chrome.storage.sync.set({ key: 'value' }, () => { console.log('Data saved'); }); // Retrieve data chrome.storage.sync.get(['key'], (result) => { console.log('Value:', result.key); }); // Listen for changes chrome.storage.onChanged.addListener((changes, area) => { console.log('Storage changed:', changes); }); |
Requires “storage” permission in manifest. Data persists across extension restarts and service worker terminations.
What tools help with Chrome extension development?
Popular development tools include: Chrome DevTools for debugging, Visual Studio Code with extensions for syntax highlighting, Plasmo framework for streamlined development, webpack or Rollup for bundling, ESLint for code quality, Prettier for formatting, TypeScript for type safety, React or Vue for UI components, Jest for testing, BrowserStack for cross-browser testing, and Chrome Extension CLI tools for automation. The Chrome Extensions documentation is your primary reference.
How long does Chrome Web Store review take?
Review typically takes 1-3 days for most extensions, though it can vary. Extensions with sensitive permissions, payment features, or first-time submissions may take longer. If your extension hasn’t been reviewed within two weeks, contact Chrome Web Store developer support. You’ll receive an email when the review is complete, whether approved or if changes are needed. Private and unlisted extensions follow the same review process.
Can I develop cross-browser extensions?
Yes, most Chromium-based browsers (Edge, Brave, Opera) support Chrome extensions with minimal or no modifications. Firefox supports WebExtensions API with some differences. Use the browser namespace instead of chrome for better compatibility, or include a polyfill. Test on each target browser. Some APIs have browser-specific implementations. The WebExtension Community Group (WECG) works on standardizing APIs across browsers.
What happens if my extension violates Chrome Web Store policies?
Policy violations can result in extension removal, developer account suspension, or permanent bans. You’ll receive an email notification explaining the violation. For first-time violations, you typically get a chance to fix issues and resubmit. Serious violations like malware distribution, user data theft, or repeated policy breaches can lead to immediate removal and account termination. Always comply with the Developer Program Policies and respond promptly to policy violation notices.
