Comparing WXT, Plasmo and CRXJS

Choose an MV3 extension framework: WXT, Plasmo and the CRXJS Vite plugin compared on manifest generation, UI frameworks, content script UI, dev reload, cross-browser builds, maintenance and lock-in.

Published October 2, 2026 Updated October 2, 2026 7 min read
Table of Contents

Starting a new extension in 2026, you can hand-roll a Vite or webpack configuration, or pick one of three popular tools: WXT, a Vite-based framework with file-based entrypoints; Plasmo, a React-first framework with its own conventions and cloud services; or CRXJS, a Vite plugin that turns your manifest into the build’s entry point. All three produce standard MV3 extensions. They differ in how much they decide for you, how they handle content-script UI, how well they build for Firefox and Safari, and how hard they are to leave. This guide compares them on the criteria that matter after the first week. It belongs to build tooling and bundlers.

Three philosophies

The tools sit at different points between “framework” and “plugin”. CRXJS is a Vite plugin: you write a real manifest.json (or a TypeScript function returning one), and the plugin follows its references — service worker, content scripts, HTML pages — bundling each and enabling hot reloading. You keep full control and full responsibility. WXT is a framework: entrypoints are discovered from file names and the manifest is generated, with per-browser targets, a typed storage layer and content-script UI helpers built in. Plasmo is the most opinionated: React components are entrypoints (popup.tsx, contents/*.tsx), it has its own content-script UI mounting, messaging and storage packages, and it integrates with a submission service. The more a tool decides, the faster you start — and the more you inherit its choices.

From plugin to frameworkHand-rolled bundler configuration, CRXJS as a Vite plugin around your manifest, WXT as a framework generating the manifest from entrypoints, and Plasmo as an opinionated React-first framework.PlasmoReact-first conventionsmost opinionatedWXTfile entrypoints, generated manifestframework-agnosticCRXJSVite plugin around manifest.jsonyou own the manifestHand-rolledVite / webpack / esbuild configfull control
Higher layers decide more for you — and are harder to leave.

Step-by-step: evaluate for your project

1. List what the extension actually needs

1needs.md
2- Surfaces: popup, options, side panel, 2 content scripts (one with injected UI)
3- UI framework: Svelte (team preference)
4- Browsers: Chrome, Edge, Firefox; Safari later
5- Hot reload of content scripts: nice to have
6- Long-term: 3+ years of maintenance, small team

Execution context: a planning document. The decision depends on these answers more than on feature checklists: a React team building a single-surface Chrome extension has different needs from a small team maintaining a cross-browser extension for years.

2. Compare on the criteria that matter

WXT, Plasmo and CRXJS comparedComparison of WXT, Plasmo and CRXJS on manifest handling, UI framework support, content-script UI, Firefox builds, development reload and lock-in.CriterionWXTPlasmoCRXJSManifestGenerated + configGenerated from package.js…You write itUI frameworksAnyReact-firstAny (Vite)Content-script UIShadow root helpersCSUI built inDo it yourselfFirefox targetBuilt inSupportedManual manifest tweaksDev reloadYesYesHMR incl. content scriptsLock-inModerateHigherLow
WXT is the balanced default; CRXJS suits teams who want to own the manifest; Plasmo suits React teams who want conventions.

3. Prototype the hardest surface in each

 1// The decisive test: injected UI in a content script, with styles isolated
 2// WXT
 3export default defineContentScript({ matches: ["https://*.example.com/*"], cssInjectionMode: "ui",
 4  async main(ctx) { (await createShadowRootUi(ctx, { name: "x-ui", position: "inline", onMount: mount })).mount(); } });
 5
 6// Plasmo (contents/fab.tsx)
 7export const config = { matches: ["https://*.example.com/*"] };
 8export default function Fab() { return <button>Save</button>; }
 9
10// CRXJS: write the shadow-root mounting yourself in content.ts and import CSS as ?inline

Execution context: throwaway prototypes. Content-script UI is where tools differ most and where bugs are hardest — style isolation, mounting, cleanup on update. Build the same small overlay in each candidate and judge the result in a page with aggressive CSS. Popups and options pages work similarly in all three.

4. Check the cross-browser output

1# Build each prototype for Firefox and inspect the manifest
2cat .output/firefox-mv3/manifest.json | jq '.background, .browser_specific_settings, .sidebar_action'

Execution context: a terminal. Confirm that the Firefox build uses background.scripts, includes your gecko id, and maps the side panel appropriately. A tool that gets this right saves a recurring source of release bugs; one that does not leaves you post-processing manifests.

Which tool fits?Decision tree: teams that want to own their manifest choose CRXJS or hand-rolled Vite; React teams wanting conventions choose Plasmo; teams needing multi-browser builds with any UI framework choose WXT.What matters most?own the manifestCRXJSor hand-rolled ViteLow lock-inmore wiringReact + conventionsPlasmofast startHigher lock-inReact-centricmulti-browser, any UIWXTbalanced defaultModerate lock-ingood targets
The team's preferences decide as much as the tools' features.

5. Estimate the cost of leaving

1Leaving checklist
2- Where does the manifest live?        (generated vs file)
3- What runtime packages ship?          (storage, messaging, UI helpers)
4- How are entrypoints declared?        (file names vs manifest references)
5- What would break if the tool stopped being maintained?

Execution context: a design review. All three produce plain extensions, but their conveniences become dependencies: Plasmo’s messaging and storage packages, WXT’s storage items and ctx, CRXJS’s dev server. A tool that is easy to leave — because your code calls chrome.* directly and the manifest is readable — is a safer long-term bet for a small team.

6. Consider maintenance signals

Look at release frequency, how quickly the tool adopts new browser APIs (side panel, user scripts, new manifest keys), the size and responsiveness of its community, and whether it has a clear maintainer. Extension tooling sits on fast-moving ground — browser APIs, Vite versions, store requirements — and a tool that stops tracking it becomes a migration project.

7. Decide and document

Write down the choice and the reason in the repository’s README: “WXT, for multi-browser targets and framework freedom; we avoid its storage wrapper so data access stays plain chrome.storage.” The second clause is the important one — it records which conveniences you deliberately did not adopt, which keeps the exit cost low.

Common mistakes

  • Choosing by starter-template polish. The first hour is not the next three years.
  • Ignoring content-script UI. It is the hardest surface and the biggest differentiator.
  • Assuming Firefox “just works”. Verify the generated Firefox manifest.
  • Adopting every helper. Each one raises the cost of leaving.
  • No snapshot of the generated manifest. Generated manifests change silently with tool upgrades.

Cross-browser variation

  • Chrome / Edge: all three tools target Chromium first and handle it well.
  • Firefox: WXT has first-class Firefox targets; Plasmo supports Firefox builds; with CRXJS you adjust the manifest for Firefox yourself.
  • Safari: all three produce a web extension folder; Safari packaging through Xcode is outside every tool’s scope.

Verification

  1. Build the same overlay prototype in each candidate and test it on a heavily styled page.
  2. Build each for Chrome and Firefox and confirm both load without manifest errors.
  3. List the runtime dependencies each adds to your bundle.
  4. Re-run the comparison when upgrading major versions.

FAQ

Can I switch tools later?

Yes, with effort proportional to how many of the tool’s runtime helpers you adopted. Keep core logic in plain modules that call chrome.* directly.

Do these tools affect store review?

No directly. Reviewers see the built extension. Bundled and minified code may need a source upload for AMO regardless of tool.

Is hand-rolled Vite still reasonable?

Yes, especially for small extensions or teams that want no framework. See bundling an MV3 extension with Vite.

Which tool handles extension updates and orphaned content scripts best?

WXT’s content-script context exposes an invalidation callback, which makes cleanup after updates straightforward. Plasmo handles mounting and unmounting of its content-script UI components. With CRXJS you implement the pattern yourself. Whichever you choose, test the update path explicitly — it is where framework abstractions most often leak.

Can I mix tools, for example CRXJS for the build and WXT-style conventions by hand?

Yes, and many teams do something similar: a plain Vite or CRXJS build with a small in-house layer for manifest generation and per-browser targets. It costs a little more setup and keeps every moving part in your own repository, which some teams value more than the convenience of a framework.

How much does the choice affect bundle size?

Very little for the extension code itself — all three use Vite and tree-shake. The difference comes from the runtime helpers you adopt, which are small, and from UI framework choices, which are not.

Other MV3 Architecture & Extension Lifecycle Resources