Overview
CRXJS provides content scripts with Vite HMR, so updates don’t always require a full page reload. Frameworks like React and Vue work in content scripts the same as in HTML pages.
The host page of a content script is the website where the content script runs.
Basic Content Script
Declare content scripts in your manifest:
{
"content_scripts" : [
{
"matches" : [ "https://*.example.com/*" ],
"js" : [ "src/content.ts" ],
"css" : [ "src/content.css" ]
}
]
}
CRXJS automatically:
Bundles your content script with dependencies
Injects the HMR client for hot updates
Handles CSS imports and static assets
Content Script Loaders
CRXJS generates loader files for content scripts to enable HMR and proper module loading.
Development Loaders
During development, loaders import from the Vite dev server:
// Loader imports preamble (for React Fast Refresh)
import '/preamble.js'
// Loader imports Vite client
import '/vite-client.js'
// Loader imports your content script
import '/src/content.ts'
Production Loaders
In production, loaders import the bundled script:
import './content-[hash].js'
From the source code (plugin-contentScripts.ts:198):
const shouldUseLoader = ! (
bundleFileInfo . type === 'chunk' &&
bundleFileInfo . imports . length === 0 &&
bundleFileInfo . dynamicImports . length === 0 &&
bundleFileInfo . exports . length === 0
)
If your content script has no imports, exports, or dynamic imports, CRXJS skips the loader and injects the code directly.
Static Assets
Import static assets freely - CRXJS automatically declares them as web_accessible_resources:
import logo from './logo.png'
import styles from './content.css?inline'
const img = document . createElement ( 'img' )
img . src = chrome . runtime . getURL ( logo )
document . body . appendChild ( img )
Content scripts share the origin of the host page. Use chrome.runtime.getURL() to convert asset imports to extension URLs.
CSS Handling
CRXJS handles content script CSS specially:
Declared CSS
{
"content_scripts" : [{
"css" : [ "src/content.css" ]
}]
}
CRXJS creates a synthetic entry point for CSS files and injects them before your JS scripts.
Imported CSS
Vite bundles imported CSS with your script.
Framework Support
React Content Scripts
import React from 'react'
import ReactDOM from 'react-dom/client'
import { App } from './App'
const root = document . createElement ( 'div' )
root . id = 'crx-root'
document . body . appendChild ( root )
ReactDOM . createRoot ( root ). render (
< React . StrictMode >
< App />
</ React . StrictMode > ,
)
React Fast Refresh works automatically. CRXJS detects React and injects the preamble code.
From the source code (plugin-contentScripts.ts:88):
if (
typeof preambleCode === 'undefined' &&
server . config . plugins . some (
({ name = 'none' }) =>
name . toLowerCase (). includes ( 'react' ) &&
! name . toLowerCase (). includes ( 'preact' ),
)
) {
const react = await import ( '@vitejs/plugin-react' )
preambleCode = react . default . preambleCode
}
Vue Content Scripts
import { createApp } from 'vue'
import App from './App.vue'
const root = document . createElement ( 'div' )
root . id = 'crx-root'
document . body . appendChild ( root )
createApp ( App ). mount ( root )
Vue HMR works out of the box.
HTML in Content Scripts
You can inject extension pages into host pages using iframes:
const src = chrome . runtime . getURL ( 'pages/iframe.html' )
const iframe = new DOMParser (). parseFromString (
`<iframe class="crx" src=" ${ src } "></iframe>` ,
'text/html' ,
). body . firstElementChild
document . body . append ( iframe )
The injected page loads in a cross-origin iframe with full Chrome API access.
You must declare the HTML file as a web-accessible resource and add it to your Vite config inputs. See Web Accessible Resources .
Imported HTML Fragments
For simple HTML without frameworks, import HTML as text:
import html from './root.html?raw'
const element = new DOMParser (). parseFromString ( html , 'text/html' ). body . firstElementChild
document . body . append ( element )
This doesn’t require web-accessible resources or Vite config changes.
Main World Content Scripts
Content scripts can run in the MAIN world (the page’s JavaScript context):
{
"content_scripts" : [{
"js" : [ "src/main-world.ts" ],
"matches" : [ "https://*.example.com/*" ],
"world" : "MAIN"
}]
}
MAIN world content scripts don’t support HMR because they run in the page context, not the extension context.
From the source code (plugin-contentScripts.ts:55):
if ( worldMainIds . size ) {
const message = colors . yellow (
`Some content-scripts don't support HMR because the world is MAIN`
)
console . log ( message )
}
Hot Module Replacement
CRXJS tracks content script dependencies and triggers HMR when they change:
From the source code (plugin-hmr.ts:159):
if (
relFiles . has ( script . id ) ||
modules . some ( isImporter ( join ( server . config . root , script . id )))
) {
relFiles . forEach (( relFile ) => update ( relFile ))
}
Content script HMR:
Updates code without reloading the page
Preserves component state (React/Vue)
Applies CSS changes instantly
HMR Timeout Configuration
Configure the HMR timeout for content scripts:
export default defineConfig ({
plugins: [
crx ({
manifest ,
contentScripts: {
hmrTimeout: 10000 , // 10 seconds (default: 5000)
},
}),
] ,
})
Web Accessible Resources Expose assets to content scripts
HMR Hot Module Replacement details
Background Scripts Communicate with service workers
Learn More