Welcome What's new in Chrome extensions Getting started API Reference Samples
Welcome What's new in Chrome extensions Getting started API Reference Samples

Content scripts

Published on Updated on

Content scripts are files that run in the context of web pages. By using the standard Document Object Model (DOM), they are able to read details of the web pages the browser visits, make changes to them, and pass information to their parent extension.

Understand content script capabilities #

Content scripts can access Chrome APIs used by their parent extension by exchanging messages with the extension. They can also access the URL of an extension's file with chrome.runtime.getURL() and use the result the same as other URLs.

// Code for displaying <extensionDir>/images/myimage.png:
var imgURL = chrome.runtime.getURL("images/myimage.png");
document.getElementById("someImage").src = imgURL;

Additionally, content scripts can access the following chrome APIs directly:

Content scripts are unable to access other APIs directly.

Work in isolated worlds #

Content scripts live in an isolated world, allowing a content script to make changes to its JavaScript environment without conflicting with the page or other extensions' content Scripts.

An isolated world is a private execution environment that isn't accessible from other extensions. A practical consequence of this isolation is that variables declared by one extension are not visible to another one. The concept was originally introduced with the initial launch of Chrome, providing isolation for browser tabs.

An extension may run in a web page with code similar to the example below.

<html>
<button id="mybutton">click me</button>
<script>
var greeting = "hello, ";
var button = document.getElementById("mybutton");
button.person_name = "Bob";
button.addEventListener("click", () =>
alert(greeting + button.person_name + ".")
, false);
</script>
</html>

That extension could inject the following content script using one of the techniques outlined in the Inject scripts section.

var greeting = "hola, ";
var button = document.getElementById("mybutton");
button.person_name = "Roberto";
button.addEventListener("click", () =>
alert(greeting + button.person_name + ".")
, false);

With this change, both alerts appear in sequence when the button is clicked.

Not only does each extension run in its own isolated world, but content scripts and the web page do too. This means that none of these (web page, content scripts, and any running extensions) can access the context and variables of the others.

Inject scripts #

Content Scripts can be injected declared statically, declared dynamically, or programmatically injected.

Inject with static declarations #

Use static content script declarations in manifest.json for scripts that should be automatically run on a well known set of pages.

Statically declared scripts are registered in the manifest under the "content_scripts" field. They can include JavaScript files, CSS files, or both. All auto-run content scripts must specify match patterns.

{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["http://*.nytimes.com/*"],
"css": ["myStyles.css"],
"js": ["contentScript.js"]
}
],
...
}
NameTypeDescription
matchesarray of stringsRequired. Specifies which pages this content script will be injected into. See Match Patterns for more details on the syntax of these strings and Match patterns and globs for information on how to exclude URLs.
cssarray of stringsOptional. The list of CSS files to be injected into matching pages. These are injected in the order they appear in this array, before any DOM is constructed or displayed for the page.
jsarray of stringsOptional. The list of JavaScript files to be injected into matching pages. These are injected in the order they appear in this array.
match_about_blankbooleanOptional. Whether the script should inject into an about:blank frame where the parent or opener frame matches one of the patterns declared in matches. Defaults to false.

Inject with dynamic declarations #

Caution

This feature is not yet fully supported. It is currently in dev and is also available in Chrome Canary.

You should use dynamic declarations in the following cases:

  • When the host is not well known
  • The script may need to be added/removed from a known host
chrome.scripting.registerContentScript(optionsObject, callback);
chrome.scripting.unregisterContentScript(idArray, callback);

Inject programmatically #

Use programmatic injection for content scripts that need to run in respond to events or on specific occasions.

In order to inject a content script programmatically, your extension needs host permissions for the page it's trying to inject scripts into. Host permissions can either be granted either by requesting them as part of your extension's manifest (see host_permissions) or temporarily via activeTab.

Below we'll look at an example that uses activeTab.

{
"name": "My extension",
...
"permissions": [
"activeTab"
],
...
}

Content scripts can be injected as files.

chrome.runtime.onMessage.addListener((message, callback) => {
if (message == "runContentScript"){
chrome.scripting.executeScript({
file: 'contentScript.js'
});
}
});

Or an entire file can be injected.

function injectedFunction() {
document.body.style.backgroundColor = 'orange';
}

chrome.runtime.onMessage.addListener((message, callback) => {
if (message == "changeColor"){
chrome.scripting.executeScript({
function: injectedFunction
});
}
});

When injecting as a function, you can also pass arguments to the function.

function injectedFunction(color) {
document.body.style.backgroundColor = color;
}

chrome.runtime.onMessage.addListener((message, callback) => {
if (message == "changeColor"){
chrome.scripting.executeScript({
function: injectedFunction,
arguments: ['orange']
});
}
});

Exclude matches and globs #

Specified page matching is customizable by including the following fields in a declarative registration.

NameTypeDescription
exclude_matchesarray of stringsOptional. Excludes pages that this content script would otherwise be injected into. See Match Patterns for more details on the syntax of these strings.
include_globsarray of stringsOptional. Applied after matches to include only those URLs that also match this glob. Intended to emulate the @include Greasemonkey keyword.
exclude_globsarray of stringOptional. Applied after matches to exclude URLs that match this glob. Intended to emulate the @exclude Greasemonkey keyword.

The content script will be injected into a page if both of the following are true:

  • Its URL matches any matches pattern and any include_globs pattern
  • The URL doesn't also match an exclude_matches or exclude_globs pattern. Because the matches property is required, exclude_matches, include_globs, and exclude_globs can only be used to limit which pages will be affected.

The following extension injects the content script into http://www.nytimes.com/ health but not into http://www.nytimes.com/ business .

{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["http://*.nytimes.com/*"],
"exclude_matches": ["*://*/*business*"],
"js": ["contentScript.js"]
}
],
...
}
chrome.scripting.registerContentScript({
id: 1,
matches: ["http://*.nytimes.com/*"],
exclude_matches: ["*://*/*business*"],
js: ["contentScript.js"]
});

Glob properties follow a different, more flexible syntax than match patterns. Acceptable glob strings are URLs that may contain "wildcard" asterisks and question marks. The asterisk * matches any string of any length, including the empty string, while the question mark ? matches any single character.

For example, the glob http:// ??? .example.com/foo/ * matches any of the following:

  • http:// www .example.com/foo /bar
  • http:// the .example.com/foo /

However, it does not match the following:

  • http:// my .example.com/foo/bar
  • http:// example .com/foo/
  • http://www.example.com/foo

This extension injects the content script into http:/www.nytimes.com/ arts /index.html and http://www.nytimes.com/ jobs /index.html but not into http://www.nytimes.com/ sports /index.html.

{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["http://*.nytimes.com/*"],
"include_globs": ["*nytimes.com/???s/*"],
"js": ["contentScript.js"]
}
],
...
}
chrome.scripting.registerContentScript({
id: 1,
matches: ['http://*.nytimes.com/*'],
include_globs: ['*nytimes.com/???s/*'],
js: ['contentScript.js']
});

This extension injects the content script into http:// history .nytimes.com and http://.nytimes.com/ history but not into http:// science .nytimes.com or http://www.nytimes.com/ science .

{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["http://*.nytimes.com/*"],
"exclude_globs": ["*science*"],
"js": ["contentScript.js"]
}
],
...
}
chrome.scripting.registerContentScript({
id: 1,
matches: ['http://*.nytimes.com/*'],
exclude_globs: ['*science*'],
js: ['contentScript.js']
});

One, all, or some of these can be included to achieve the correct scope.

{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["http://*.nytimes.com/*"],
"exclude_matches": ["*://*/*business*"],
"include_globs": ["*nytimes.com/???s/*"],
"exclude_globs": ["*science*"],
"js": ["contentScript.js"]
}
],
...
}
chrome.scripting.registerContentScript({
matches: ['http://*.nytimes.com/*'],
exclude_matches: ['*://*/*business*'],
include_globs: ['*nytimes.com/???s/*'],
exclude_globs: ['*science*'],
js: ['contentScript.js']
});

Run time #

The run_at field controls when JavaScript files are injected into the web page. The preferred and default value is "document_idle", but you can also specify "document_start" or "document_end" if needed.

{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["http://*.nytimes.com/*"],
"run_at": "document_idle",
"js": ["contentScript.js"]
}
],
...
}
chrome.scripting.registerContentScript({
matches: ['http://*.nytimes.com/*'],
run_at: 'document_idle',
js: ['contentScript.js']
});
NameTypeDescription
document_idlestringPrefered. Use "document_idle" whenever possible.

The browser chooses a time to inject scripts between "document_end" and immediately after the window.onload event fires. The exact moment of injection depends on how complex the document is and how long it is taking to load, and is optimized for page load speed.

Content scripts running at "document_idle" do not need to listen for the window.onload event, they are guaranteed to run after the DOM is complete. If a script definitely needs to run after window.onload, the extension can check if onload has already fired by using the document.readyState property.
document_startstringScripts are injected after any files from css, but before any other DOM is constructed or any other script is run.
document_endstringScripts are injected immediately after the DOM is complete, but before subresources like images and frames have loaded.

Specify frames #

The "all_frames" field allows the extension to specify if JavaScript and CSS files should be injected into all frames matching the specified URL requirements or only into the topmost frame in a tab.

{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["http://*.nytimes.com/*"],
"all_frames": true,
"js": ["contentScript.js"]
}
],
...
}
chrome.scripting.registerContentScript({
matches: ['http://*.nytimes.com/*'],
all_frames: true,
js: ['contentScript.js']
});
NameTypeDescription
all_framesbooleanOptional. Defaults to false, meaning that only the top frame is matched.

If specified true, it will inject into all frames, even if the frame is not the topmost frame in the tab. Each frame is checked independently for URL requirements, it won't inject into child frames if the URL requirements are not met.

Communication with the embedding page #

Although the execution environments of content scripts and the pages that host them are isolated from each other, they share access to the page's DOM. If the page wishes to communicate with the content script, or with the extension via the content script, it must do so through the shared DOM.

An example can be accomplished using window.postMessage:

var port = chrome.runtime.connect();

window.addEventListener("message", (event) => {
// We only accept messages from ourselves
if (event.source != window)
return;

if (event.data.type && (event.data.type == "FROM_PAGE")) {
console.log("Content script received: " + event.data.text);
port.postMessage(event.data.text);
}
}, false);
document.getElementById("theButton").addEventListener("click", () => {
window.postMessage({ type: "FROM_PAGE", text: "Hello from the webpage!" }, "*");
}, false);

The non-extension page, example.html, posts messages to itself. This message is intercepted and inspected by the content script and then posted to the extension process. In this way, the page establishes a line of communication to the extension process. The reverse is possible through similar means.

Stay secure #

While isolated worlds provide a layer of protection, using content scripts can create vulnerabilities in an extension and the web page. If the content script receives content from a separate website, such as making an XMLHttpRequest, be careful to filter content cross-site scripting attacks before injecting it. Only communicate over HTTPS in order to avoid "man-in-the-middle" attacks.

Be sure to filter for malicious web pages. For example, the following patterns are dangerous:

Don't

var data = document.getElementById("json-data")
// WARNING! Might be evaluating an evil script!
var parsed = eval("(" + data + ")")

Don't

var elmt_id = ...
// WARNING! elmt_id might be "); ... evil script ... //"!
window.setTimeout("animate(" + elmt_id + ")", 200);

Instead, prefer safer APIs that do not run scripts:

Do

var data = document.getElementById("json-data")
// JSON.parse does not evaluate the attacker's scripts.
var parsed = JSON.parse(data);

Do

var elmt_id = ...
// The closure form of setTimeout does not evaluate scripts.
window.setTimeout(() => animate(elmt_id), 200);

Last updated: Improve article

We serve cookies on this site to analyze traffic, remember your preferences, and optimize your experience.