C.W.K.
Stream
Lesson 01 of 06 · published

What Content Scripts Are and Why They Exist

~10 min · content-script, isolated-world, dom, concepts

Level 0Extension Curious
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"The service worker lives in the browser. The popup lives in your toolbar. The content script is the only piece of ClipDeck that actually lives inside the page the user is reading. Track 3 walks into that room and turns on the lights."

The Three Living Rooms

By the end of Track 2, ClipDeck had two living rooms — the service worker (event-driven background) and the popup (transient toolbar UI). Both are extension-owned. Neither has direct access to github.com's DOM, the user's selected paragraph on Wikipedia, or the button they're hovering on Hacker News.

That access lives only in the third room: a content script. It runs inside the host tab, alongside the page's own JavaScript, with full DOM read/write privileges. It's how Chrome extensions reach into pages they don't own.

What You Can Do With Them

Practical examples, all things content scripts make possible:

  • Read the user's selected text (ClipDeck's Track 3 milestone).
  • Inject a floating button or sidebar into every page.
  • Re-style elements — dark mode for sites that don't ship one, hide ads, collapse cookie banners.
  • Listen for clicks, hovers, keyboard shortcuts at page level.
  • Scrape structured data (price tags, article body) and pipe it to your SW for storage.
  • Modify forms before submit — autofill, validation, sanitization.

Anything that requires being physically present in the DOM at page-load time falls here. No other context (SW, popup, side panel) can substitute.

The Isolation Trade

Content scripts are not free — they pay a price for that DOM access. They live in an 'isolated world' (covered in Lesson 3): same DOM, but a separate JavaScript heap from the page itself. window.myLib defined by the host page is invisible to your content script. console.log in your script writes to the content-script console, not the page's. The two worlds talk only through DOM events and the message channel.

The isolation is what makes content scripts safe to use on arbitrary, untrusted pages. The page can't redefine functions in your script. Your script can't accidentally trample the page's globals. The boundary is sharp on purpose.

How They Reach Storage

Content scripts have a subset of the chrome.* API — enough to talk to the service worker but not enough to be a self-contained extension. They can:

  • Send messages: chrome.runtime.sendMessage.
  • Receive messages: chrome.runtime.onMessage.
  • Read/write extension storage: chrome.storage.local (the same storage the SW reads).

They cannot register chrome.tabs.onUpdated, open new tabs, or use most extension-control APIs. Those stay with the SW. The pragmatic ClipDeck pattern: content script reads the DOM (selection, page metadata), packages it, sends to the SW via message, SW writes to storage. The SW is the only context that touches storage as the system of record; the content script is a sensor.

The service worker is the brain (events, storage, lifecycle). The popup is the face (user interaction). The content script is the hand inside the page (DOM access). Three contexts, three responsibilities, one extension.
What about the side panel? Track 4. The side panel is the popup's bigger sibling — same powers, persistent layout instead of a tooltip. Content script is still the hand. The side panel just gives the hand more room to display what it grabbed.

Code

content.js — read DOM, ship to SW via message·javascript
// content.js — a minimum-viable content script
// Runs inside the host tab; has DOM access.
console.log("[ClipDeck content] hi from", location.href);

// Read the page title from inside the page
const pageTitle = document.title;
console.log("[ClipDeck content] title:", pageTitle);

// Send what we saw back to the SW
chrome.runtime.sendMessage({
  type: "contentScriptHello",
  url: location.href,
  title: pageTitle,
});
manifest.json — declare a content script that runs on every URL·json
{
  "manifest_version": 3,
  "name": "ClipDeck",
  "version": "0.4.0",
  "action": { "default_popup": "popup.html" },
  "background": { "service_worker": "background.js" },
  "permissions": ["storage", "tabs"],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "run_at": "document_idle"
    }
  ],
  "icons": { "16": "icons/16.png", "48": "icons/48.png", "128": "icons/128.png" }
}

External links

Exercise

Create clipdeck/content.js containing the first code block. Update clipdeck/manifest.json with the second code block (version bumped to 0.4.0, content_scripts entry added). Reload the extension at chrome://extensions. Open any real web page (try a wikipedia.org article). Open the page's DevTools (right-click → Inspect → Console). In the dropdown at the top-left of the Console panel, switch the context from 'top' to 'ClipDeck' — you should see the [ClipDeck content] logs there, NOT in 'top'. That dropdown is your single most important content-script debugging tool.
Hint
If you don't see the logs at all, confirm content.js was actually loaded — chrome://extensions → ClipDeck → 'Errors' will list any syntax errors. The <all_urls> match pattern won't match chrome://, chrome-extension://, or the New Tab page; navigate to a real http/https URL. If the Console dropdown only shows 'top' and no 'ClipDeck' option, the content script didn't inject — the most common cause on a freshly edited manifest is forgetting to reload the extension. The handler isn't 'oh it'll pick up next page load' — it's 'click the reload button in chrome://extensions first.'

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.