Content scripts
Build page-integrated extension features with content scripts while keeping a reliable dev loop for JS and CSS updates.
Extension.js compiles content script entries from manifest.json, wraps them for runtime mounting/HMR behavior, and emits predictable content_scripts/* outputs.
Content script capabilities
Supported manifest fields
Sample content script declaration
Below is an example of how to declare content scripts within the manifest.json file:
Authoring contract
For every content-script-like entry, Extension.js expects a mount-style default export:
- The module should
export defaulta synchronous function. - That function should perform setup work.
- It may return a synchronous cleanup callback.
- Classes are not supported as the default export.
This applies to:
- files referenced by
manifest.json > content_scripts[*].js - files placed under the project
scripts/folder when they are used as script entrypoints
Valid shapes
Invalid shapes
Async guidance
Keep the default export synchronous even when the feature does async work internally. Start async work inside the function and return a synchronous cleanup.
Why this matters: Extension.js remounts content scripts during development. Without a cleanup contract, it is easy to duplicate UI, event listeners, observers, and timers.
What happens on contract violations
- No default export: Extension.js warns during development and skips mounting.
- Default export is not a function: Extension.js warns during development and skips mounting.
- Async default export: the module still runs, but the returned
Promiseis not treated as cleanup.
If your content script appears to compile but never mounts, check the default export first.
Runtime wrapper behavior
- Content script modules are wrapped with mount/runtime helpers.
- Dev mode adds HMR accept/dispose behavior and remount flow.
- CSS updates trigger remount events (
__EXTENSIONJS_CSS_UPDATE__) in development. run_attiming is respected from manifest values.
scripts/ folder behavior
The scripts/ folder is for script entrypoints that are not declared by an HTML page entry. In practice, these entries follow the same mount-style runtime expectations as content scripts.
That means scripts/ is not just a generic folder for loose JavaScript files:
- script entry files should still have a default export function when they are meant to mount behavior
- adding or removing supported files under
scripts/is treated as a structural change in watch mode - Extension.js may require a dev-server restart when that entry set changes
Output path
Content script entries are normalized per manifest index:
Main world notes
world: "MAIN"has dedicated bridge/runtime behavior and different extension API limitations.- MAIN-world scripts use bridge-based chunk loading/public-path handling in development.
- Treat MAIN world as an advanced path and validate behavior on target browsers early.
Isolated vs MAIN quick example
Use isolated world by default. Use MAIN only when page-context access is required and account for extension API/runtime constraints.
Matching and execution guidance
The browser still controls where a content script runs. Extension.js bundles the file, but the matching contract still comes from the manifest entry.
- Keep
matchesas narrow as the feature allows. - Add
exclude_matches,all_frames, ormatch_about_blankonly when the feature actually requires those behaviors. - Treat
run_atandworldas part of the feature contract, not just implementation detail. - Re-test permission and host-permission scope when changing where a content script runs.
Development behavior
- Editing content script code usually updates through wrapper-driven HMR/remount flow.
- CSS-only entries receive dev helper behavior so style updates can propagate.
- If content script entrypoint lists change in manifest, Extension.js can require dev server restart.
Best practices
- Keep content script entry files small and delegate logic to shared modules.
- Scope selectors/styles carefully to avoid host-page collisions.
- Prefer explicit
run_atandworldvalues when behavior depends on timing/context. - Treat manifest content-script list changes as structural development changes.
- Pass page-derived data through validated messaging instead of performing privileged work directly in the page boundary.
- Default to isolated world and move to
MAINonly when the page context is strictly required.
Next steps
- Understand update outcomes in dev update behavior.
- Design cross-context communication with Messaging.
- Review access scope in Permissions and host permissions.
- Learn about web-accessible resources.
- Continue with locales in development.
