Skip to content
Back to Project Tutorials

Browser Extension

Build a Manifest V3 extension for Chrome/Firefox and publish to the Web Store.

Design & Manifest

Define extension type, permissions, and the manifest.json that gates everything.

Why

Manifest V3 is mandatory in Chrome — the manifest dictates your permissions, CSP, and capabilities, and the Web Store reviews it strictly. Over-requesting permissions triggers review rejection and user distrust; under-requesting breaks your feature. Get this contract right first.

When

Before writing any content/popup/background code. 2-5 days to finalize the manifest + permission scope.

How

1. Choose extension surface - Popup: click icon -> small HTML panel (most common) - Content script: inject into pages (DOM modification, scraping) - Background service worker (MV3): events, alarms, cross-tab state - Side panel / DevTools panel: specialized UIs 2. Write manifest.json (MV3) - manifest_version: 3 - permissions: request the minimum (activeTab, storage, alarms) - host_permissions: only the domains you actually touch - action.default_popup for the popup HTML - background.service_worker for the SW 3. Enforce content security policy - MV3 default CSP disallows remote code (no eval, no remote scripts) - All code must be bundled in the extension package - Use a bundler (Vite/webpack) to produce a single dist/ 4. Plan cross-browser (Chrome + Firefox) - Firefox supports MV3 with browser.* APIs (promise-based) - Use webextension-polyfill to unify chrome.* and browser.* - Test both from the start; diverge only with browser detection 5. Scope permissions for review - activeTab (granted on click) > tabs (always-on) — prefer activeTab - Justify each permission in the store listing description

Example

manifest.json (MV3 popup + content script): { "manifest_version": 3, "name": "Page Tidy", "version": "1.0.0", "action": { "default_popup": "popup.html" }, "background": { "service_worker": "sw.js" }, "permissions": ["activeTab", "storage", "alarms"], "host_permissions": ["https://*.example.com/*"], "content_scripts": [{ "matches": ["https://*.example.com/*"], "js": ["content.js"] }] } Bundler (Vite): src/ -> dist/ with manifest copied in Cross-browser: webextension-polyfill unifies chrome/browser APIs

Common Problems

  • ⚠Requesting broad host permissions (<all_urls>) — triggers slow review and scares users
  • ⚠Using eval/remote scripts — MV3 CSP rejects them at runtime
  • ⚠Building for Chrome only, then discovering Firefox MV3 differences at publish time

Best Practices

  • ✓Request activeTab instead of tabs — it grants access only on user action, easing review
  • ✓Bundle everything with Vite; MV3 forbids remote code, so no CDN scripts at runtime
  • ✓Use webextension-polyfill from day 1 so Chrome and Firefox share one codebase

Development & Testing

Build popup/content/SW with hot reload and test across browsers.

Why

Extension bugs are environment-specific: service workers suspend after 30s of idle, content scripts race with page JS, and popup state resets on every close. Without testing these lifecycles, you ship bugs that only appear in real browsing sessions.

When

After manifest is finalized. Build the popup UI first (fastest feedback loop), then content script, then SW.

How

1. Set up dev with hot reload - vite + @crxjs/vite-plugin: HMR for popup, content scripts - Load unpacked: chrome://extensions -> Developer mode -> Load dist/ 2. Build the popup - React/Vue/Svelte popup, keep it < 300KB (loads on every click) - Persist state to chrome.storage.local, not in-memory (popup resets) 3. Build content script - Match specific URLs only; inject on document_idle to avoid races - Communicate with SW via chrome.runtime.sendMessage - Isolate styles with Shadow DOM to avoid polluting page CSS 4. Handle service worker lifecycle - SW suspends after ~30s idle — never hold state in SW memory - Use chrome.storage for persistence, chrome.alarms for scheduled work - Re-establish listeners on every wake (events API is idempotent) 5. Test across browsers - Load in Chrome (chromium) and Firefox (about:debugging) - Vitest for unit logic; Playwright with the extension for E2E - Test SW suspension: idle 60s, then trigger an event

Example

Popup state persistence: // popup.tsx — load on open, save on change const [prefs, setPrefs] = useState<Prefs>(); useEffect(() => { chrome.storage.local.get('prefs').then(r => setPrefs(r.prefs)); }, []); const update = (p) => chrome.storage.local.set({ prefs: p }); SW alarm (survives suspension): chrome.alarms.create('sync', { periodInMinutes: 30 }); chrome.alarms.onAlarm.addListener(a => { if (a.name === 'sync') doSync(); }); E2E (Playwright + extension): const ctx = await chromium.launchPersistentContext('', { args: [`--load-extension=${distPath}`], });

Common Problems

  • ⚠Holding state in SW memory — lost when it suspends, causing intermittent 'first click does nothing' bugs
  • ⚠Content script CSS leaks into the page — breaks host site styling and gets uninstalled
  • ⚠Popup bundle > 1MB — slow open, users perceive the extension as laggy

Best Practices

  • ✓Treat the service worker as stateless — persist to chrome.storage, rehydrate on every event
  • ✓Wrap content script UI in a Shadow DOM root so your styles never collide with the page
  • ✓Keep the popup bundle tiny (< 300KB); code-split heavy views and lazy-load on demand

Store Publishing

Package, sign, and publish to Chrome Web Store and Firefox Add-ons (AMO).

Why

The Web Store is your only distribution channel — a rejected submission blocks all users. Store review enforces permission justification, privacy practice, and single-purpose policy. A clean first submission builds trust; repeated rejections can get your developer account flagged.

When

When the extension is tested across browsers and the manifest permissions are justified. Budget 1-7 days for Chrome review, < 2 days for Firefox.

How

1. Package the extension - vite build -> dist/ with manifest.json - Zip dist/ (Chrome) or sign with web-ext (Firefox) - Include a 128x128 icon and store assets (screenshots, promo tile) 2. Create store listings - Chrome Web Store: $5 one-time dev fee, then upload .zip - Firefox AMO: free, sign with web-ext sign --api-key/--api-secret - Write clear description justifying each permission 3. Prepare privacy practice - Privacy policy URL required if you collect any data - State what data is collected, where it's stored, how it's used - "Single purpose" statement: one sentence describing the extension's purpose 4. Submit and handle review - Chrome: upload zip, submit for review, watch the developer dashboard - Address rejection feedback precisely — resubmit only the fixed item - Firefox: signed xpi auto-published on AMO after review 5. Post-publish updates - Upload new zip for updates (Chrome re-reviews, usually faster) - Use chrome.storage.sync for user settings backup across devices - Monitor uninstall reasons via the Web Store dashboard

Example

Package + publish (Chrome): vite build # -> dist/ cd dist && zip -r ../ext.zip . # package # Upload ext.zip at chrome.google.com/webstore/devconsole Sign + publish (Firefox): web-ext build # -> web-ext-artifacts/*.xpi web-ext sign --api-key $KEY --api-secret $SECRET # Signed xpi auto-publishes on AMO Listing description (permission justification): "Permissions: - activeTab: read the current tab when you click the icon - storage: remember your preferences locally No data leaves your device."

Common Problems

  • ⚠No privacy policy despite collecting data — instant rejection and possible account flag
  • ⚠Vague single-purpose statement — reviewers reject extensions that 'do too many things'
  • ⚠Resubmitting without addressing the specific review feedback — cycles for weeks

Best Practices

  • ✓Write the privacy policy and single-purpose statement before packaging — they're review requirements
  • ✓Justify every permission in the listing description; reviewers read this first
  • ✓Address rejection feedback point-by-point and resubmit only the fixed version, not a rewrite

Full Developer Lifecycle

A 14-stage developer roadmap from idea to maintenance — visualize dependencies and track progress.

Next Step