/library / template-chrome-extension-mv3
templateTooling

Chrome MV3 extension with TypeScript + Vite

A Manifest V3 extension built with Vite + TypeScript. Content script, background service worker, options page, and popup all bundled together. Cross-browser-ready (Chrome, Edge, Firefox).

use whenBrowser extension that augments specific sites, captures data, or adds a side-panel tool. You want a real build system, not raw JS files.

May 2, 20262,120 byteschromeextensionmv3vitebrowser

[Extension Name]

A Manifest V3 browser extension. Popup, options page, content script, background worker. TypeScript end to end.

Source of truth

GitHub. Versioned zips uploaded to Chrome Web Store via web-ext sign (also works for Firefox). The current store version is the source of truth for what users have.

Tech stack

Vite 6 (with @crxjs/vite-plugin for MV3 HMR) + TypeScript + React 19 (for the popup + options pages). Tailwind v4. Manifest V3. Storage via chrome.storage.local. Cross-browser polyfilled via webextension-polyfill.

Deploy

  1. npm run build -> outputs to dist/
  2. cd dist && zip -r ../extension.zip .
  3. Upload to Chrome Web Store Developer Dashboard
  4. (Optional) web-ext sign --api-key=$AMO_KEY --api-secret=$AMO_SECRET for Firefox AMO

File map

  • src/manifest.ts declarative manifest (compiled to JSON by crxjs)
  • src/background.ts MV3 service worker (event-driven, NOT persistent)
  • src/content/ content scripts injected per-site
  • src/popup/ React popup
  • src/options/ React options page
  • src/lib/storage.ts typed chrome.storage wrapper
  • src/lib/messaging.ts content <-> background message types
  • vite.config.ts with @crxjs/vite-plugin
  • public/icons/ 16/32/48/128 PNG icons

.env keys

None at runtime (MV3 has no env injection). Build-time variables can be passed via Vite define.

Hard rules

  • MV3 background is a service worker. It can be killed at any time. Persist anything via chrome.storage, not module-level variables.
  • Content scripts run in an isolated world. They cannot read the page's JS scope without injecting a script tag.
  • host_permissions should be the minimum needed. Users see them at install. Asking for "all sites" tanks adoption.
  • All cross-context messaging is typed via src/lib/messaging.ts. No raw chrome.runtime.sendMessage calls.
  • Store icons must be 16x16, 32x32, 48x48, 128x128 PNG. Inkscape -> PNG export.
  • Privacy policy URL required for any extension that handles user data.

Recent significant changes

  • 2026-05-02: Scaffolded. Locked: Vite + crxjs (HMR matters), React for popup (consistency with rest of stack), webextension-polyfill for Firefox parity.

Next session: start here

  1. Edit src/manifest.ts name, description, host_permissions.
  2. Generate icons (Figma export or realfavicongenerator.net adapted).
  3. npm run dev -> Vite watches, crxjs hot-reloads the unpacked extension.
  4. Load dist/ as unpacked extension at chrome://extensions.
  5. Test on real sites before submitting to store.

Get the next CLAUDE.md in your inbox.

One new template every week, plus occasional case studies.