manifest.json and modern source into a working extension. Extension.js is a CLI framework that owns its bundler. CRXJS is a plugin that adds extension output to a Vite setup you own. This page is a factual comparison so you can choose based on your project.
Which should you choose?
Choose Extension.js if…
- You want one CLI that opens the browser, reloads on save, builds per browser, and zips for the stores.
- You ship to Chrome, Edge, Firefox, and Safari from one
manifest.json. - You want no bundler config in your project.
Choose CRXJS if…
- You already have a Vite app and want extension output added to it.
- You depend on Vite plugins and want to keep them.
- You are fine loading the unpacked build in the browser yourself.
At a glance
Mental model
Extension.js owns the bundler. You write amanifest.json and reference real files. The CLI compiles them, filters the manifest for each browser, and packages the result. You never configure the bundler, and bundler upgrades are the framework’s job.
CRXJS lives inside your bundler. You add crx({ manifest }) to vite.config, and Vite builds the extension next to the rest of your app. You keep full control of Vite and every Vite plugin. Each Vite major needs CRXJS compatibility work, which is why the Vite 8 fileName error existed for a while.
Dev loop
Extension.js
CRXJS
Cross-browser builds
- Extension.js builds every browser from one source with
--browser. - Browser differences go in browser-prefixed manifest fields, such as
chrome:andfirefox:. - CRXJS builds one target per run, and
browser: 'firefox'in the plugin options gives a Firefox build. - The CRXJS docs have no Safari guide.
Packaging
dist/<browser> and a zip per browser next to it. With CRXJS, you zip Vite’s output folder with your own script or tool.
When to migrate
Move from CRXJS to Extension.js when you want the CLI to launch browsers, build several browsers in one run, or ship to Safari. Your components,chrome.* calls, and styles carry over. Migrate from CRXJS walks through it step by step.

