Main-world Content Script

Injects a small UI into every web page from both the extension and the page's own JavaScript world, with an options page that moves it to the left or right edge.

Runs on Chrome, Edge, Firefox · Manifest V3 · Surfaces: content script, options page, background service worker · Styling: CSS · Level: intermediate

Create this project

npx extension@latest create my-content-main-world --template content-main-world
cd my-content-main-world
npm install
npm run dev

!Powered by Extension.js

Content Script in MAIN World Example

Injects a small UI into every web page from both the extension and the page's own JavaScript world, with an options page that moves it to the left or right edge.

What you'll see: A small UI injected into any web page, isolated in a Shadow DOM so site styles don't bleed through, carrying an Open options button. The options page has one checkbox, and ticking it sends the UI to the left edge of the page straight away.

How it works: A content script mounts a JavaScript UI inside a Shadow DOM and applies scoped styles so the host page can't bleed through.

Loads the content script in the page's MAIN world (Chromium-only). Useful when your script needs direct access to page-side globals or classes that the isolated world cannot reach. Firefox does not support MAIN-world content scripts, so this template is gated to Chromium targets.

The manifest also registers an options_ui page bundled from src/options/. The options page reads the setting with chrome.storage.sync.get on load and writes it with chrome.storage.sync.set on change.

Here is the part worth copying. chrome.storage and chrome.runtime do not exist in the MAIN world, so the injected UI cannot read the setting and cannot message the background worker. A second content script, src/content/isolated-options.js, is registered without a world key and therefore runs in the ISOLATED world, where both APIs are available. That companion owns the storage read, the chrome.storage.onChanged subscription, and the relay to the background worker. The two halves talk over window.postMessage on a shared channel from src/content/utils/bridge.js.

Either half can load first, so both paths exist: the companion publishes the value as soon as it loads, and the MAIN world half asks for it once its own listener is attached. Both halves remove their listeners in the cleanup function Extension.js calls on teardown.

That window belongs to the page, which can read and forge these messages. The payloads are therefore trivial, they carry nothing secret, and the MAIN world half narrows any value it receives to one of the two edges before using it.

Try it locally

npx extension@latest create my-content-main-world --template content-main-world
cd my-content-main-world
npm install
npm run dev

A fresh browser window opens with the extension already loaded.

Project layout

src/
├── content/
│   ├── utils/
│   │   ├── bridge.js
│   │   ├── constants.js
│   │   └── create-badge.js
│   ├── isolated-one.js
│   ├── isolated-options.js
│   ├── isolated-two.js
│   ├── scripts.js
│   └── styles.css
├── images/
│   └── icon.png
├── options/
│   ├── index.html
│   ├── scripts.js
│   └── styles.css
├── background.js
└── manifest.json

Commands

Cloned this repo instead? The examples ship without npm scripts, so run Extension.js directly from the example directory. Run npm install first when the example declares dependencies.

dev

Run the extension in development mode. Target a browser with --browser:

npx extension@latest dev .                  # Chromium (default)
npx extension@latest dev . --browser=chrome
npx extension@latest dev . --browser=edge
npx extension@latest dev . --browser=firefox

build

Build for production:

npx extension@latest build .                # Chromium (default)
npx extension@latest build . --browser=firefox
npx extension@latest build . --browser=edge

preview

Preview the production build with the bundled browser:

npx extension@latest preview .

Tests

This template ships an end-to-end check (template.spec.ts) validated by the examples-repo CI on every commit.

Learn more

extension.devDocumentation for extension.dev