crxjs
CRXJS Chrome extension development — true HMR for popup, options, content scripts, side panels, manifest-driven builds, dynamic content script imports (`?script`, `?script&module`), and `defineManifest` for type-safe manifests. Uses Vite as its build tool. Use when the user mentions CRXJS, crxjs, @c
By samber · 2,005 installs
npx skills add samber/cc-skills --skill crxjs
Source repository · Upstream listing
CRXJS
CRXJS is a Chrome extension development tool that provides true HMR for popup, options, content scripts, and side panels. It reads your manifest to auto generate the extension output, handles content script injection, and manages the service worker build. Under the hood it is a Vite plugin ( @crxjs/vite plugin ).
Current status
Package : @crxjs/vite plugin (v2.x stable, latest v2.4.0 as of March 2026)
Scaffolding : npm create crxjs@latest (always use @latest )
Maintained by : @Toumash and @FliPPeDround (since mid 2025)
GitHub : github.com/crxjs/chrome extension tools (~4k stars)
Vite compatibility : v3 through v8 beta
Quick start
Vite config by framework
CRXJS is added as a Vite plugin. The setup varies slightly per framework.
React
Use @vitejs/plugin react (not plugin react swc ) for best HMR compatibility. If you must use SWC, cast the manifest:
Vue
Svelte
Vanilla TypeScript
defineManifest — type safe dynamic manifest
Instead of a static JSON file, use CRXJS's defineManifest for dynamic values and full TypeScript autocompletion:
Import in vite.config.ts:
Type declarations
Add to a src/vite env.d.ts or src/crxjs.d.ts :
This enables types for ?script and ?script&module imports.
HMR behavior by context
Context HMR How it works
Popup Full HMR WebSocket based, state preserved
Options page Full HMR Same as popup
Side panel Full HMR Same as popup
Content script (manifest) True HMR CRXJS injects loader + HMR client
Content script (dynamic) True HMR Via ?script import
Service worker Auto reload Changes trigger full extension reload
Main world scripts No HMR Skipped by CRXJS loader
Content script HMR works because CRXJS generates a loader script that imports an HMR preamble, the HMR client, and your actual script — enabling real module level HMR without full page reload. This is CRXJS's main differentiator.
Dynamic content script imports
For content scripts injected programmatically (not in manifest), CRXJS provides special import suffixes:
For main world injection (no HMR):
CRXJS plugin options
Development workflow
After loading once, subsequent npm run dev sessions reconnect automatically. No need to re load the extension unless manifest.json changes.
Production build
The dist/ directory is ready to zip and upload to Chrome Web Store:
Disable Vite's module preload to avoid CWS rejection of inline scripts:
Known issues and workarounds
Tailwind CSS HMR in content scripts
New Tailwind classes may not trigger CSS updates in content scripts. Workaround : restart dev server after adding new utility classes. Improved in v2.4.0 but not fully resolved. Ensure injectCss: true in config.
WebSocket connection errors ( ws://localhost:undefined/ )
Cause : port mismatch between dev server and HMR config. Fix : explicitly set both to the same value:
"Manifest version 2 is deprecated" warning
If you see this, your manifest is being interpreted as MV2. Fix : ensure "manifest version": 3 is set.
Content scripts not injecting on file:// URLs
Chrome requires the user to enable "Allow access to file URLs" in the extension settings at chrome://extensions. CRXJS cannot change this.
HMR stops working after Chrome update
CRXJS's HMR relies on injecting a content script that connects to the dev server's WebSocket. Chrome security updates occasionally break this. Fix : update to the latest CRXJS version, which tracks Chrome changes.
CRXJS vs alternatives
Feature CRXJS WXT Plasmo
Content script HMR True HMR File based reload Partial
Framework support Any Vite framework Any React focused
Abstraction level Thin (Vite plugin) Full framework Full framework
Messaging helpers None (use chrome.\ directly) Built in Built in
Storage wrappers None Built in Built in
Cross browser Chrome + Firefox Chrome + Firefox + Safari Chrome + Firefox
File based routing No Yes Yes
Learning curve Low (know Vite, know CRXJS) Medium Medium
Choose CRXJS when : you want minimal abstraction over raw Chrome APIs and value content script HMR above all. CRXJS stays out of the way — no magic routing, no wrapper APIs, just your code with HMR.
Choose WXT when : you want conventions, built in utilities, and cross browser support.
Choose Plasmo when : you're React focused and want the highest level abstraction.
Project structure (recommended)
CRXJS resolves HTML files referenced in the manifest automatically. Your popup.html can use standard <script type="module" src="./main.tsx" and it works.
If you encounter a bug or unexpected behavior in CRXJS, open an issue at github.com/crxjs/chrome extension tools/issues.