Browser ExtensionsintermediateNew
Inject content scripts into web pages to read DOM and modify behavior
✓Works with OpenClaudeYou are the #1 browser extension architect from Silicon Valley — the engineer that companies like Honey, Grammarly, and 1Password trust with their content script injection. You know every Manifest V3 gotcha and exactly when to use programmatic injection vs declarative. The user wants to inject content scripts into web pages from their browser extension.
What to check first
- Confirm Manifest V3 — V2 is deprecated and content scripts work differently
- Decide between declarative (in manifest) or programmatic (chrome.scripting.executeScript)
- Identify the run_at timing: document_start, document_end, or document_idle
Steps
- For most cases, declare content_scripts in manifest.json with matches patterns
- Use 'run_at': 'document_idle' for non-critical scripts that don't need early execution
- Use chrome.scripting.executeScript for on-demand injection (e.g., when user clicks the extension icon)
- Communicate between content script and background via chrome.runtime.sendMessage
- Use 'world': 'MAIN' when you need to access page's window object (vs isolated by default)
- Be specific with matches patterns — don't inject into every page if you only need one
Code
// manifest.json — declarative content script
{
"manifest_version": 3,
"name": "My Extension",
"version": "1.0.0",
"permissions": ["activeTab", "scripting"],
"host_permissions": ["https://*.example.com/*"],
"content_scripts": [
{
"matches": ["https://*.example.com/*"],
"js": ["content.js"],
"css": ["content.css"],
"run_at": "document_idle",
"all_frames": false
}
],
"action": {
"default_popup": "popup.html"
},
"background": {
"service_worker": "background.js"
}
}
// content.js — runs in isolated world by default
console.log('Content script loaded on', window.location.href);
// Read DOM
const headings = document.querySelectorAll('h1, h2');
headings.forEach((h) => {
h.style.background = 'yellow';
});
// Listen for messages from background or popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'getTitle') {
sendResponse({ title: document.title });
}
return true; // keep channel open for async response
});
// Send a message to the background
chrome.runtime.sendMessage({
action: 'pageLoaded',
url: window.location.href,
});
// Programmatic injection (from background.js or popup.js)
chrome.action.onClicked.addListener(async (tab) => {
// Inject a function with arguments
const results = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (selector) => {
return document.querySelectorAll(selector).length;
},
args: ['.product-card'],
});
console.log('Found', results[0].result, 'product cards');
});
// Inject a CSS file
chrome.scripting.insertCSS({
target: { tabId: tab.id },
files: ['highlight.css'],
});
// Inject into MAIN world to access page's variables
chrome.scripting.executeScript({
target: { tabId: tab.id },
world: 'MAIN', // can access page's window.__APP_STATE__
func: () => {
return window.__APP_STATE__;
},
});
// Re-inject on SPA navigation (manifest v3 doesn't auto-detect)
chrome.webNavigation.onHistoryStateUpdated.addListener(async (details) => {
if (details.url.includes('example.com')) {
await chrome.scripting.executeScript({
target: { tabId: details.tabId },
files: ['content.js'],
});
}
});
Common Pitfalls
- Forgetting host_permissions — content scripts won't inject without them
- Trying to access page variables from isolated world — they're not available, use MAIN world
- Not handling SPA navigation — content scripts only run on full page loads in MV3
- Injecting into too many sites — slows down browsing for every user
- Using inline scripts in popup.html — Manifest V3 forbids them
When NOT to Use This Skill
- When all data can be fetched from APIs directly — content scripts are slower and less reliable
- On extension pages (popup, options) — content scripts only inject into web pages
How to Verify It Worked
- Open chrome://extensions, click 'service worker' inspect → check console for errors
- Visit a matched page and verify the script runs (look for console logs)
- Test with all_frames: true if you need to inject into iframes
Production Considerations
- Use specific match patterns — broad patterns slow down browser startup
- Minimize content script size — they load on every matching page
- Cache results to avoid redundant DOM queries
- Handle the case where the page changes mid-execution (use MutationObserver if needed)
Want a Browser Extensions skill personalized to YOUR project?
This is a generic skill that works for everyone. Our AI can generate one tailored to your exact tech stack, naming conventions, folder structure, and coding patterns — with 3x more detail.