Content Scripts in Chrome Extensions

12 min read

Content Scripts in Chrome Extensions

Content scripts are a powerful feature of Chrome extensions that run in the context of web pages. They allow your extension to read and modify page content, enabling a wide range of functionality from ad blocking to page enhancement. This guide covers everything you need to master content scripts.

How Content Scripts Work

Content scripts are JavaScript files that Chrome injects into web pages that match patterns you specify. Unlike regular JavaScript on a webpage, content scripts can access and manipulate the DOM directly. They run in an isolated world, which provides security benefits but also means they cannot directly access page variables.

Key Concepts

  • Injected JavaScript: Content scripts run in the context of the page
  • DOM Access: Full access to read and modify page content
  • Isolated World: Separate JavaScript execution environment from the page
  • Match Patterns: URL patterns that determine when to inject

Declaration in Manifest V3

{
  "content_scripts": [
    {
      "matches": ["https://*.example.com/*", "<all_urls>"],
      "js": ["content.js"],
      "css": ["styles.css"],
      "run_at": "document_idle"
    }
  ]
}

The “matches” array defines which pages will have your script injected. You can use specific URLs, wildcards, or the special “" pattern.

Match Patterns

Understanding match patterns is crucial:

Pattern Matches
https://example.com/* All HTTPS pages on example.com
https://*.google.com/* All Google subdomains
https://example.com/page.html Specific page only
<all_urls> Every webpage
file:///C:/path/* Local files

Types of Content Script Injection

Declarative Injection

As shown above, you declare content scripts in the manifest. Chrome automatically injects them based on URL patterns. This is the most common approach and works well for extensions that need to run on specific sites.

{
  "content_scripts": [
    {
      "matches": ["https://*.google.com/*"],
      "js": ["google-content.js"],
      "css": ["google-styles.css"],
      "run_at": "document_end"
    },
    {
      "matches": ["https://*.github.com/*"],
      "js": ["github-content.js"],
      "run_at": "document_idle"
    }
  ]
}

Programmatic Injection

You can also inject content scripts programmatically from background scripts or other extension contexts:

// Inject a content script when needed
chrome.scripting.executeScript({
  target: { tabId: tabId },
  files: ['content.js']
}, (results) => {
  console.log('Script injected successfully');
});

// Or inject a function directly
chrome.scripting.executeScript({
  target: { tabId: tabId },
  func: () => {
    console.log('Running in page context');
    return document.title;
  }
}, (results) => {
  console.log('Page title:', results[0].result);
});

Programmatic injection requires the “scripting” permission and is triggered by user action or extension events.

Accessing Page Content

Content scripts have access to the page’s DOM but run in an isolated world. This creates a unique environment with specific characteristics:

// Reading page content
const heading = document.querySelector('h1');
console.log('Page title:', heading.textContent);

// Finding multiple elements
const links = document.querySelectorAll('a');
links.forEach(link => console.log(link.href));

// Modifying the page
const newElement = document.createElement('div');
newElement.textContent = 'Added by my extension!';
newElement.className = 'my-extension-element';
document.body.appendChild(newElement);

// Changing styles
const header = document.querySelector('header');
if (header) {
  header.style.backgroundColor = '#f0f0f0';
  header.style.padding = '10px';
}

// Removing elements
document.querySelectorAll('.advertisement').forEach(el => el.remove());

Isolation Characteristics

Content scripts in their isolated world can:

  • Read and modify the DOM freely
  • Add their own JavaScript functions
  • Use Chrome extension APIs (storage, runtime, etc.)
  • Not access variables defined by page scripts
  • Not be accessed by page scripts directly
// This variable is private to the content script
const myPrivateData = 'secret';

// Page scripts cannot access this
// window.myPrivateData === undefined

Communication with Extension

Content scripts can communicate with other parts of your extension using message passing:

Sending Messages

// Send message to background script
chrome.runtime.sendMessage({
  type: 'PAGE_DATA',
  data: { 
    url: window.location.href,
    title: document.title,
    timestamp: Date.now()
  }
});

// Listen for response
chrome.runtime.sendMessage(
  { type: 'GET_SETTINGS' },
  (response) => {
    console.log('Settings:', response);
  }
);

Receiving Messages

// Listen for messages from background or popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'updateContent') {
    document.body.style.backgroundColor = message.color;
    sendResponse({ success: true });
  }
  
  if (message.action === 'getPageInfo') {
    sendResponse({
      url: window.location.href,
      title: document.title,
      ready: document.readyState
    });
  }
  
  return true; // Keep channel open for async response
});

Timing of Injection

Control when your content script runs using the “run_at” option:

  • “document_start” - Before any DOM is created, CSSOM is available
  • “document_end” - After DOM is complete but before resources load
  • “document_idle” - After DOM and resources (default)
{
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["content.js"],
    "css": ["early-styles.css"],
    "run_at": "document_start"
  },
  {
    "matches": ["<all_urls>"],
    "js": ["content-late.js"],
    "run_at": "document_idle"
  }]
}

When to Use Each Timing

  • document_start: For injecting CSS, modifying meta tags, or pre-loading scripts
  • document_end: For most DOM manipulations, when you need the DOM but not images
  • document_idle: Default, safest choice for most use cases

Isolated Worlds

Each content script runs in its own isolated JavaScript world. This provides significant security benefits:

  • Page JavaScript cannot access your content script’s variables
  • Your content script cannot access page JavaScript’s variables
  • CSS is automatically isolated

This isolation protects your code from conflicts with page scripts, but also means you can’t directly share data through JavaScript variables.

Communicating Through DOM

You can still interact with page scripts through shared DOM elements:

// Create a custom event that page scripts can listen to
const event = new CustomEvent('myExtensionReady', { 
  detail: { data: 'hello' } 
});
document.dispatchEvent(event);

// Or listen for page events
window.addEventListener('pageReady', (e) => {
  console.log('Page ready:', e.detail);
});

Communicating Through DOM

// Set a property on the window that page scripts can access
window.myExtensionAPI = {
  getData: () => ({ url: location.href }),
  onAction: (callback) => {
    document.addEventListener('extensionAction', callback);
  }
};

// The page can then use:
const data = window.myExtensionAPI.getData();

Common Use Cases

Content scripts are perfect for:

  1. Page modification - Adding UI elements, hiding content, changing styles
  2. Data extraction - Scraping information from pages
  3. Form enhancement - Auto-filling forms, adding validation
  4. Ad blocking - Removing or hiding advertisement elements
  5. Page analytics - Tracking user interactions
  6. Accessibility improvements - Adding keyboard navigation, ARIA labels
  7. Reading tools - Changing fonts, colors, layout for readability

Practical Example: Page Highlighter

// content.js - Highlight specific elements on a page
function highlightElements(selector, color = 'yellow') {
  const elements = document.querySelectorAll(selector);
  elements.forEach(el => {
    el.style.backgroundColor = color;
    el.dataset.extensionHighlighted = 'true';
  });
  return elements.length;
}

// Listen for highlight requests from popup/background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'highlight') {
    const count = highlightElements(message.selector, message.color);
    sendResponse({ highlighted: count });
  }
});

Best Practices

Match Specific URLs

Avoid using “" unless necessary. Specific URL patterns reduce performance impact and increase user trust:

{
  "content_scripts": [{
    "matches": [
      "https://*.github.com/*",
      "https://github.com/*"
    ],
    "js": ["github-content.js"]
  }]
}

Clean Up After Yourself

If you add elements or modify styles, consider cleaning them up when appropriate:

// Remove added elements on page unload
window.addEventListener('unload', () => {
  document.querySelectorAll('.my-extension-element').forEach(el => el.remove());
});

// Restore modified styles
const originalStyles = new Map();
function cleanupStyles() {
  originalStyles.forEach((original, element) => {
    element.style.cssText = original;
  });
  originalStyles.clear();
}

Handle Dynamic Content

Use MutationObserver for pages with dynamic content:

const observer = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    mutation.addedNodes.forEach((node) => {
      if (node.nodeType === Node.ELEMENT_NODE) {
        // Process new elements
        if (node.matches('.dynamic-content')) {
          processDynamicElement(node);
        }
        
        // Check children too
        node.querySelectorAll('.dynamic-content').forEach(processDynamicElement);
      }
    });
  });
});

function processDynamicElement(element) {
  if (element.dataset.processed) return;
  element.dataset.processed = 'true';
  
  // Your processing logic here
  element.classList.add('extension-processed');
}

observer.observe(document.body, { 
  childList: true, 
  subtree: true 
});

Avoid Conflicts with Page Scripts

// Use unique class names to avoid conflicts
const EXTENSION_PREFIX = 'myext-';

// Wrap your code in an IIFE
(function() {
  // Your code here
})();

// Use explicit scoping
{
  const privateVariable = 'safe';
}

Conclusion

Content scripts are fundamental to building powerful Chrome extensions that enhance web pages. Understanding their isolated nature, communication methods, and best practices will help you create extensions that work reliably across different websites while maintaining security and performance.

Remember these key points:

  • Always use specific URL match patterns
  • Clean up after yourself
  • Handle dynamic content properly
  • Communicate effectively with other extension parts
  • Test across multiple websites
No previous article
No next article