# web-extension-starter generates five browser builds from one manifest

> web-extension-starter is a React 19 and Vite 7 template for browser extensions where the interesting part is not the React but the build: one manifest.json with vendor prefixes produces a Chrome, Firefox, Opera, Edge or Brave artefact, content scripts are bundled as IIFE because that execution environment has no module support, and the dev scripts are watch builds rather than a dev server.

**abhijithvijayan/web-extension-starter** — 🖥🔋Web Extension starter to build "Write Once Run on Any Browser" extension

- Repository: https://github.com/abhijithvijayan/web-extension-starter
- Stars: 2,189 · Forks: 177
- Language: TypeScript
- License: MIT
- Published: 2026-09-29 · Updated: 2026-09-29 · Language: en
- Canonical page: https://hysenlabs.com/projects/abhijithvijayan-web-extension-starter

## One manifest template, five generated manifests

The centre of the project is a single manifest file in the source folder, and it contains keys that no browser understands. They are vendor prefixes, and the build step expands them into the manifest each store expects:

```json
{
  "__chrome__name": "My Chrome Extension",
  "__firefox__name": "My Firefox Addon",
  "__chrome|firefox__description": "Works on both!"
}
```

The double underscore prefix names one vendor, the pipe names several, and everything without a prefix is shared. The expansion is done by vite-plugin-wext-manifest, a plugin also written by the same author and listed as the manifest layer in the tech stack. This is the piece that removes the tedium, because the alternative is a manifest per target with the same fields copied and drifting apart, and because a browser store rejection for a wrong name or a missing key is a slow kind of failure to discover. Everything downstream of that is conventional: Vite 7 for the build, React 19 with the automatic JSX runtime for the UI, TypeScript 5.9 by default, SCSS with CSS Modules for styling, and ESLint 9 in flat config with Prettier. Nothing about React is required, but nothing stops you from deleting the popup.

## Manifest V3 everywhere, with Firefox 112 as the floor

The starter targets Manifest V3 for every browser, and the browser table gives the numbers: Chrome 88 or newer, Firefox 112 or newer, Opera 74 and Edge 88 as Chromium-based browsers, and the latest Brave. The Firefox number is not arbitrary and the README explains why, because Firefox 112 is the version that supports Manifest V3 together with ES modules in background scripts. If you need to support older Firefox there is a separate guide, docs/FIREFOX_MV2.md, for going back to Manifest V2. That is the honest shape of cross-browser extension work in 2026: one manifest version, five stores, and a set of shared-denominator APIs through webextension-polyfill, which is a declared dependency. The consequence to plan for is that the service worker lifecycle on Chromium and the background script lifecycle on Firefox are different models, and the starter gives you a Background folder that is described as a service worker on Chrome and a background script on Firefox. Anything that relies on the worker being long-lived needs checking on both.

## Content scripts are bundled as IIFE on purpose

One implementation note in the README explains more about this template than any feature list. Content scripts are automatically bundled as IIFE, an immediately invoked function expression, to ensure compatibility with the browser's content script execution environment, which does not support ES modules. That is a real constraint rather than a stylistic choice: your popup and options page are normal pages and can use modules, while a content script is injected into someone else's page and inherits that environment's rules. If you have written an extension before with a bundler and had a content script import another file, you have probably already met this. The project structure makes the separation visible, with source folders for Background, ContentScript, Popup, Options, components, styles, types, utils, public static assets, and the manifest.json template at the root of source. Four extension surfaces, one shared component directory, and a build that knows which of them is which.

## The dev scripts are watch builds, so reload by hand

Getting a first build is three commands after installing dependencies, and each script targets one browser:

```bash
# Install dependencies
npm install

# Start development server
npm run dev:chrome    # For Chrome
npm run dev:firefox   # For Firefox

# Build for production
npm run build:chrome  # Build Chrome extension
npm run build:firefox # Build Firefox addon
npm run build         # Build for all browsers
```

What those scripts actually are is worth knowing before you expect a dev server. The dev entries run cross-env with a TARGET_BROWSER variable set to chrome or firefox and then vite build in watch mode, which is a rebuild on change rather than a server proxying to the browser. The feature list calls it hot module reloading, and the loading instructions are consistent with a watch build: in Chrome you go to chrome://extensions, enable developer mode, click Load unpacked and select the extension/chrome directory; in Firefox you go to about:debugging, click This Firefox, then Load Temporary Add-on and pick extension/firefox/manifest.json. In practice that means you reload the extension in the browser's own page after a change. The build: script simply runs build:chrome and then build:firefox, and the output lands in per-browser directories, which is why the load instructions name a path per target.

## The template is a tarball, and the branch name matters

Using it does not mean cloning. The README gives one command that downloads a tarball and strips the top level directory into your new folder:

```bash
curl -fsSL https://github.com/abhijithvijayan/web-extension-starter/archive/react-typescript-vite.tar.gz | tar -xz --strip-components=1
```

Two details in that line are worth reading. It points at a branch archive, react-typescript-vite, not at the default branch, so the template you get is the one that branch names rather than whatever is currently on main, and the tarball has no history, which is what you want in a project you own from the first commit. The --strip-components=1 flag removes the archive's top directory, so your package.json lands in the folder you are standing in rather than in a nested one. The repository itself also carries a .nvmrc, an .editorconfig, a postcss config for autoprefixer, a flat ESLint config in eslint.config.mjs, and a Code of Conduct. Node 20 or later is the documented requirement, and package.json enforces it with an engines field.

## The stack is current, and it depends on the author's own packages

The versions are current, which has two sides. On one side you start on React 19.2, Vite 7, TypeScript 5.9, ESLint 9.39 and the React Vite plugin, with a dependency set of react, react-dom, webextension-polyfill and advanced-css-reset, and that is a stack with no known security notes attached to it in the repository. On the other, two dev dependencies are the author's own published packages, @abhijithvijayan/eslint-config at version 3 and @abhijithvijayan/tsconfig at 1.5, and eslint-plugin-import-x, eslint-plugin-jsx-a11y, eslint-plugin-n and eslint-config-prettier are configured on top of them. The lint setup is genuinely thorough for a starter, with import ordering, JSX accessibility, Node rules and formatting all enforced, which is the kind of thing you would otherwise add in month two. The cost is a supply chain that includes packages under one person's npm scope, and a template that will need those packages updated when they stop. For a starter that is a reasonable trade; for a product, copying the config into your own repository is the safer move.

## Version 3.0.0, and who is shipping on it

The release history is short. The visible tags are 1.0.0 from 2019-11-16, 1.1.0 from 2019-12-30 and v3.0.0 from 2026-02-03, and package.json reports version 3.0.0, which matches the newest tag. The last push to main was 2026-09-13, so master is ahead of the 3.0.0 release, and there is a six-year gap between the 1.1.0 tag and 3.0.0 that the release list does not explain, so the project's own history is not a reliable guide to how often it changes. What the README does say is that four extensions are shipped in production on this starter and that they have over 100,000 users between them, naming daily.dev, Jiffy Reader, kutt-extension and doubanIMDb. Read that as evidence of adoption rather than of quality: a large install base says the template survives contact with a store review, and it says nothing about whether the last React upgrade landed. The licence is MIT with a LICENCE file at the repository root, so there is no obligation beyond keeping the notice.

## Against a hand-written manifest and against an extension framework

There are two other reasonable starting points. Writing manifest.json by hand with a background service worker and a content script costs nothing, has no build step, and is the right answer for an extension that only injects a script or rewrites a header; the moment you add a React popup, a Sass file and a second browser, the hand-written route starts costing you the same five manifests this template automates. The other option is a dedicated extension framework such as WXT, which is also Vite-based and generates the manifest from an entrypoint convention rather than from a prefixed template, or Plasmo, which is React-first and ships more scaffolding around the UI. The differences that matter in practice are the size of the convention you accept and the browser coverage you get for free. This template asks for almost nothing: a source folder layout, a manifest with prefixed keys, and a watch build per browser. That is a smaller commitment than adopting a framework's project model, and a smaller set of features when you need something the framework would have handed you.

## Conclusion

Adopt web-extension-starter if you are writing an extension that has to exist in more than one store and you would rather not maintain five manifests, because the vendor prefix mechanism and the cross-env TARGET_BROWSER build matrix remove the part that is pure repetition. Do not adopt it for a single-browser extension with no UI, where a hand-written manifest and a background service worker are less machinery. Verify four things before you commit: that you accept Manifest V3 as the floor, which the README ties to Firefox 112 or newer, that the dev scripts are watch builds so you reload the extension after each change, that your Node is 20 or newer, and which npm packages you are inheriting, since the template depends on shared eslint and tsconfig packages published under the author's own scope. The project is MIT, the package version is 3.0.0, the v3.0.0 release is dated 2026-02-03, and the last push was 2026-09-13.

## FAQ

### How do I create a project from web-extension-starter?

Run curl -fsSL https://github.com/abhijithvijayan/web-extension-starter/archive/react-typescript-vite.tar.gz piped to tar with --strip-components=1, which downloads a branch tarball and unpacks it into the current directory. Then run npm install with Node 20 or newer.

### How does web-extension-starter build a Firefox version from one manifest?

The manifest template uses vendor prefix keys such as __chrome__name, __firefox__name and __chrome|firefox__description, and vite-plugin-wext-manifest expands them per target. The build scripts set a TARGET_BROWSER variable through cross-env and run vite build for that target.

### Which browsers and versions does web-extension-starter support?

Manifest V3 across the board: Chrome 88 or newer, Firefox 112 or newer, Opera 74 and Edge 88 as Chromium-based browsers, and the latest Brave. Firefox 112 is the floor because that is where Manifest V3 with ES modules in background scripts is supported, and docs/FIREFOX_MV2.md covers older Firefox with Manifest V2.

### Why are content scripts bundled as IIFE in web-extension-starter?

Because the content script execution environment does not support ES modules, so the build wraps them as immediately invoked function expressions. The popup and options pages are ordinary pages and are not affected.

### How do I load the extension for development?

In Chrome, open chrome://extensions, enable developer mode, click Load unpacked and select the extension/chrome directory. In Firefox, open about:debugging, click This Firefox, then Load Temporary Add-on and select extension/firefox/manifest.json. The dev scripts are watch builds, so reload the extension after a change.

### What licence is web-extension-starter released under?

MIT, declared in package.json with a LICENCE file at the repository root. The package version is 3.0.0, matching the v3.0.0 release from 2026-02-03.

## Sources

- [abhijithvijayan/web-extension-starter on GitHub](https://github.com/abhijithvijayan/web-extension-starter)
- [Issues](https://github.com/abhijithvijayan/web-extension-starter/issues)
- [License: MIT](https://github.com/abhijithvijayan/web-extension-starter/blob/main/LICENSE)
- [README](https://github.com/abhijithvijayan/web-extension-starter/blob/main/README.md)
- [Releases](https://github.com/abhijithvijayan/web-extension-starter/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/abhijithvijayan-web-extension-starter
