Skip to main content
Extension.js and Plasmo both compile a browser extension from modern source, reload it while you work, and package it for the stores. They differ in where the manifest lives, which bundler does the work, and how actively each one ships. This page is a factual comparison so you can choose based on your project, not marketing copy.

TL;DR: which should you choose?

Choose Extension.js if…

  • You want the manifest.json as the source of truth, not generated from file names.
  • You want the CLI to launch the browser and load the extension for you.
  • You target Chrome, Edge, Firefox, and Safari from one tree, with per-browser dist/ output.
  • You want a bundler that still receives releases, and docs that your assistant can read through a docs MCP endpoint and llms.txt (details).

Choose Plasmo if…

  • You prefer file-system convention (popup.tsx, contents/) over an explicit manifest.
  • You rely on CSUI, which mounts a React, Svelte, or Vue component into a page inside a shadow root for you.
  • You use @plasmohq/storage and @plasmohq/messaging, and your extension already works on the pinned toolchain.
The rest of this page backs up that summary, dimension by dimension.

At a glance

Mental model

Extension.js stays close to the platform. You author a manifest.json and reference real files. The CLI compiles, filters per browser, and ships. If you already understand how a browser extension is structured, the framework gets out of your way. Plasmo abstracts the platform. A file named popup.tsx becomes the popup. A file under contents/ becomes a content script, and its matches come from an exported config object. Plasmo’s own docs describe it as “like Next.js for browser extensions”. The manifest is generated under the hood from your source files and from the manifest field in package.json. Neither approach is universally better. The choice depends on whether you want to see your manifest or declare your manifest.

CLI surface

Extension.js

extension dev launches the browser on a temporary profile and loads the build. The argument can be a local path, a GitHub URL, or a ZIP archive. See Get started immediately.

Plasmo

plasmo dev writes build/chrome-mv3-dev and starts a live-reloading server. Loading that folder into the browser is a manual step: the Plasmo docs say “We plan to automate this in the future.” Every browser and manifest pair is one --target value.

Cross-browser strategy

Both frameworks ship one codebase to several browsers, with different mechanics:
  • Extension.js uses browser-prefixed manifest fields (chrome:, firefox:, gecko:, and so on) inside one manifest.json. The unprefixed keys apply everywhere, and prefixed keys land only in matching builds. One run with --browser=chrome,firefox writes dist/chrome and dist/firefox.
  • Plasmo builds one target per run. Browser differences live in environment variables: a .env.firefox file, process.env.PLASMO_BROWSER in code, and env placeholders inside the manifest overrides. A placeholder that resolves to nothing removes the field from the generated manifest.
Plasmo’s officially supported targets are chrome-mv3, firefox-mv2, and the experimental firefox-mv3. Its FAQ says Edge, Brave, and Opera “should work” because they are Chromium. Safari needs the safari-mv3 target plus a manual run of Apple’s converter. Extension.js names each of those browsers as a --browser value, and the Safari build runs the converter and xcodebuild for you.

Where Plasmo is ahead

Be honest about this before you move:
  • CSUI. Export a React, Svelte 3, or Vue 3 component from a content script. Plasmo mounts it in a shadow root, isolated from the host page’s styles. In Extension.js you write the container and the shadow root yourself, as shown in Content scripts.
  • Store submission from the repo. Plasmo documents a GitHub Action, Browser Platform Publisher, that reads a keys.json secret and uploads to the stores. Extension.js builds the archives (Package an extension for store upload). The upload is left to your CI against each store’s API. The extension publish command produces a share link on extension.dev, not a store submission.
  • Bundled helper libraries. @plasmohq/storage and @plasmohq/messaging wrap chrome.storage and chrome.runtime messaging. Extension.js has no equivalent packages. You call the browser APIs directly, which is also why @plasmohq/storage keeps working after a migration.

Maintenance status

Check the signals yourself before you decide. These values come from npm and the GitHub API on 5 October 2026. Plasmo still installs and still builds. Downloads stay high because existing projects keep it pinned. There has been no release and no commit on main in over sixteen months. If your extension works today, you do not need to move. If you start a new extension, or need a bundler that still receives updates, read on.

Migration paths

If you are already on Plasmo and want to evaluate Extension.js, the typical migration touches four things:
  1. Write a manifest.json by hand, mapping each convention file to a manifest entry and merging the manifest field from package.json.
  2. Move content script matches from the exported PlasmoCSConfig into content_scripts in the manifest.
  3. Rename PLASMO_PUBLIC_* to EXTENSION_PUBLIC_*.
  4. Replace plasmo dev, plasmo build, and plasmo package with extension dev and extension build --zip.
Your React components, Tailwind config, tests, and chrome.* calls copy over without changes. The full step-by-step guide, with the convention-to-manifest table and the CSUI replacement, is at Migrate from Plasmo.

When to choose Extension.js

  • You want the manifest as the source of truth, not generated.
  • You want the CLI to launch the browser and load the extension for you.
  • You target Chrome, Edge, Firefox, and Safari from one tree and one command.
  • You want a bundler that still receives releases.
  • You want a docs MCP endpoint and llms.txt for your assistant.

When to choose Plasmo

  • You prefer file-system convention over an explicit manifest.
  • You depend on CSUI anchors and the shadow-root mounting that it does for you.
  • You already publish through Browser Platform Publisher and your pinned toolchain still builds.

See also