Labsco
samber logo

crxjs

157

by samber · part of samber/cc-skills

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, @crxjs/vite-plugin, 'extension with hot reload', 'HMR for chrome extension', or wants to set up a CRXJS-based Chrome extension project with any framework (React, Vue, Svelte, Solid, Vanilla). Also...

🔥🔥🔥✓ VerifiedFreeNeeds API keys
🧩 One of 7 skills in the samber/cc-skills package — works on its own, and pairs well with its siblings.

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, @crxjs/vite-plugin, 'extension with hot reload', 'HMR for chrome extension', or wants to set up a CRXJS-based Chrome extension project with any framework (React, Vue, Svelte, Solid, Vanilla). Also...

Inspect the full instructions your agent will receiveExpand

This is the exact playbook injected into your agent when the skill activates — shown here so you can audit it before installing. You don't need to read it to use the skill.

by samber

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, @crxjs/vite-plugin, 'extension with hot reload', 'HMR for chrome extension', or wants to set up a CRXJS-based Chrome extension project with any framework (React, Vue, Svelte, Solid, Vanilla). Also... npx skills add https://github.com/samber/cc-skills --skill crxjs Download ZIPGitHub157

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

Vite config by framework

CRXJS is added as a Vite plugin. The setup varies slightly per framework.

React

Copy & paste — that's it
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";

export default defineConfig({
 plugins: [react(), crx({ manifest })],
});

Use @vitejs/plugin-react (not plugin-react-swc) for best HMR compatibility. If you must use SWC, cast the manifest:

Copy & paste — that's it
import { ManifestV3Export } from "@crxjs/vite-plugin";
const manifest = manifestJson as ManifestV3Export;

Vue

Copy & paste — that's it
import vue from "@vitejs/plugin-vue";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";

export default defineConfig({
 plugins: [vue(), crx({ manifest })],
});

Svelte

Copy & paste — that's it
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";

export default defineConfig({
 plugins: [svelte(), crx({ manifest })],
});

Vanilla TypeScript

Copy & paste — that's it
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";

export default defineConfig({
 plugins: [crx({ manifest })],
});

defineManifest — type-safe dynamic manifest

Instead of a static JSON file, use CRXJS's defineManifest for dynamic values and full TypeScript autocompletion:

Copy & paste — that's it
// manifest.ts
import { defineManifest } from "@crxjs/vite-plugin";
import pkg from "./package.json";

export default defineManifest((config) => ({
 manifest_version: 3,
 name: config.command === "serve" ? `[DEV] ${pkg.name}` : pkg.name,
 version: pkg.version,
 description: pkg.description,
 permissions: ["storage", "activeTab", "scripting"],
 action: {
 default_popup: "src/popup/index.html",
 default_icon: {
 "16": "public/icons/icon16.png",
 "48": "public/icons/icon48.png",
 },
 },
 background: {
 service_worker: "src/background/index.ts",
 type: "module",
 },
 content_scripts: [
 {
 matches: ["https://*/*"],
 js: ["src/content/index.ts"],
 css: ["src/content/styles.css"],
 },
 ],
 options_page: "src/options/index.html",
 side_panel: { default_path: "src/sidepanel/index.html" },
 icons: {
 "16": "public/icons/icon16.png",
 "48": "public/icons/icon48.png",
 "128": "public/icons/icon128.png",
 },
}));

Import in vite.config.ts:

Copy & paste — that's it
import manifest from "./manifest";
// ... crx({ manifest })

Type declarations

Add to a src/vite-env.d.ts or src/crxjs.d.ts:

Copy & paste — that's it
/// 

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:

Copy & paste — that's it
// background.ts — ?script gives you a resolved path for executeScript
import contentScript from "./content?script";

chrome.action.onClicked.addListener(async (tab) => {
 await chrome.scripting.executeScript({
 target: { tabId: tab.id! },
 files: [contentScript],
 });
});

For main world injection (no HMR):

Copy & paste — that's it
import mainWorldScript from "./inject?script&module";

await chrome.scripting.executeScript({
 target: { tabId },
 world: "MAIN",
 files: [mainWorldScript],
});

CRXJS plugin options

Copy & paste — that's it
crx({
 manifest,
 browser: "chrome", // 'chrome' | 'firefox'
 contentScripts: {
 injectCss: true, // auto-inject CSS for content scripts
 hmrTimeout: 5000, // HMR connection timeout (ms)
 },
});

Development workflow

Copy & paste — that's it
# Start dev server (outputs to dist/ with HMR)
npm run dev

# 1. Open chrome://extensions
# 2. Enable "Developer mode"
# 3. Click "Load unpacked"
# 4. Select the dist/ directory
# 5. Edit code — popup/content scripts update instantly via HMR
# 6. Service worker changes trigger automatic extension reload

After loading once, subsequent npm run dev sessions reconnect automatically. No need to re-load the extension unless manifest.json changes.

Production build

Copy & paste — that's it
npm run build # outputs to dist/

The dist/ directory is ready to zip and upload to Chrome Web Store:

Copy & paste — that's it
cd dist && zip -r ../extension.zip .

Disable Vite's module preload to avoid CWS rejection of inline scripts:

Copy & paste — that's it
build: {
 modulePreload: false;
}

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)

Copy & paste — that's it
my-extension/
├── src/
│ ├── background/
│ │ └── index.ts
│ ├── content/
│ │ ├── index.ts
│ │ └── styles.css
│ ├── popup/
│ │ ├── index.html 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.