DEV Community

ExtensionBooster
ExtensionBooster

Posted on

Chrome Extension Content Scripts: Manifest V3 Guide (2026) | ExtensionBooster

Chrome Extension Content Scripts: Manifest V3 Guide (2026) | ExtensionBooster

UI design in Chrome extensions is where most developers struggle. After building several, here's what I've learned about making extensions look good and work well.

The essentials

  • Chrome Extension Content Scripts: Manifest V3 Guide (2026) | ExtensionBooster You’ve spent two hours debugging why your content script can’t read a page variable
  • You’ve confirmed the variable exists using DevTools
  • Then it hits you: your content script runs in an isolated world
  • The page’s JavaScript and your script are sharing the same DOM but living in completely separate JavaScript contexts

Here's the thing

Chrome Extension Content Scripts: Manifest V3 Guide (2026) | ExtensionBooster You’ve spent two hours debugging why your content script can’t read a page variable. You’ve confirmed the variable exists using DevTools. You’ve triple-checked the selector. Then it hits you: your content script runs in an isolated world. It literally cannot see that variable. The page’s JavaScript and your script are sharing the same DOM but living in completely separate JavaScript contexts. That isolation is not a bug. But knowing when it helps you, and when it fights you, is what separates a working extension from a frustrating one. This guide covers everything you need: how the two execution worlds work, every injection method, the chrome. scripting API, match patterns, and a practical section on calling chrome. downloads from a content script context. Isolated World vs Main World: The Core Concept When your content script runs on a page, Chrome places it in an isolated world by default. Picture two people working at the same desk (the DOM) but wearing noise-canceling headphones. They can both move the same papers (DOM nodes), but they can’t hear each other’s conversations (JavaScript variables and functions). Concretely: Your content script has its own JavaScript heap. It shares document and window. document for DOM manipulation. data , or any variable the page’s own scripts set. The page cannot call your functions or access your variables. This separation is what lets Chrome extensions work safely on any site. Your extension code won’t accidentally collide with the site’s libraries, and a malicious site can’t hijack your extension’s logic. Here’s the comparison you’ll want to bookmark: Isolated World (default) Main World ( world: 'MAIN' ) JS context Separate from page Shared with page Access page variables No Yes Page can access your code No Yes chrome. dom Almost none Messaging to service worker Yes No (must use custom events or postMessage) Stable since Always Chrome 111 Use when Most cases Interacting with page JS (e. , SPA state, third-party SDKs) Gotcha: When you switch to world: 'MAIN' , you gain access to page JS but lose access to most Chrome APIs. ) from a main-world script. Use a coordinating isolated-world script that listens for window. postMessage events from your main-world script. Static Injection: Declaring Scripts in manifest. json The simplest way to inject a content script is a static declaration in your manifest. Chrome reads this at install time and injects automatically on matching pages. { "manifest_version" : 3 , "name" : "My Extension" , "version" : "1. 0" , "content_scripts" : [ { "matches" : [ "https://. com/" ], "js" : [ "content. css" ], "run_at" : "document_idle" , "world" : "ISOLATED" , "all_frames" : false } ] } The key fields: matches : Array of match patterns. Controls which URLs get your script. ":///" covers all HTTP/HTTPS; "" adds file:// and more. Broad patterns trigger extra scrutiny in Chrome Web Store review. js : Array of script files, injected in order. css : Stylesheets injected before the DOM is ready (before document_start ). "document_idle" (default) fires after the page’s DOMContentLoaded and a short grace period. "document_start" fires before any DOM is parsed. "document_end" fires after DOM is ready but before sub-resources (images, scripts) finish loading. world : "ISOLATED" (default) or "MAIN". all_frames : Default false. Set true to inject into iframes too. Static injection is ideal for extensions that always need to run on specific sites. The content script loads automatically; no user action required. scripting Sometimes you don’t want a script running on every page load. Maybe you inject on demand when the user clicks the extension icon, or conditionally based on page content. You’ll need these permissions in your manifest: { "permissions" : [ "scripting" ], "host_permissions" : [ "https://. com/" ] } Or use "activeTab" instead of broad host permissions for click-triggered injection. activeTab grants temporary access to the currently active tab when the user invokes your extension. One-off injection with executeScript // From your service worker or popup async function injectOnCurrentTab () { const [ tab ] = await chrome. query ({ active: true , currentWindow: true }); await chrome. executeScript ({ target: { tabId: tab. js" ], world: "ISOLATED" // or "MAIN" }); } You can also inject a function directly instead of a file: await chrome. executeScript ({ target: { tabId: tab. id }, func : () => { document. background = "red" ; } }); Note: the injected function runs in an isolated context. Variables from the surrounding service worker closure are not available inside func. Pass data via the args parameter if needed. Persistent registration with registerContentScripts For scripts that should persist across browser restarts (similar to static injection but set up at runtime): await chrome. registerContentScripts ([{ id: "my-script" , matches: [ "https://. js" ], runAt: "document_idle" , world: "ISOLATED" }]); You can update or unregister these later with chrome. updateContentScripts and chrome. scripting reference for all options.

💡 Quick tip: Keep popup UI minimal — users close it fast if it's slow to load.


If you build Chrome extensions

You'll eventually need tools for icons, screenshots, MV2→MV3 conversion, and bundle analysis.

ExtensionBooster has free versions of all of these — I keep it bookmarked for every extension project.

Top comments (0)