Chrome ExtensionsReactCRXJSManifest V3

How to Build Chrome Extensions with React, Vite & CRXJS in 2026

Published 2026-02-27 · 12 min read · By the Optymized team

After shipping over 100 production versions of Chrome and Edge extensions for e-commerce companies across Europe, we have distilled our approach into this practical guide. Whether you are building your first extension or migrating to Manifest V3, this covers the stack, patterns, and pitfalls we have learned the hard way.

1. Why browser extensions are underrated

Browser extensions sit at a unique intersection: they have access to the pages your users already work in, they do not require a separate app install, and they can automate workflows that would otherwise require switching between multiple tabs and tools.

For e-commerce businesses, this is particularly powerful. An extension can inject price comparisons directly into a competitor's page, automate bulk operations in marketplace dashboards, or add one-click invoicing to order management panels.

The ROI is often immediate: teams report saving 2-4 hours per day on repetitive browser-based tasks after deploying a custom extension. That is time that goes straight back into growing the business.

2. The stack: Vite + CRXJS + React + TypeScript

Our production stack has been refined over 100+ extension versions. Here is what we use and why:

Vite 8

We run on Vite 8 with backward support for Vite 3, 4, 5, 6, and 7. Lightning-fast builds and native ES module support. Extension development involves constant reloading - Vite makes this near-instant. No more waiting 30 seconds for webpack to rebuild. Staying on the latest version means you always get the newest optimizations and security patches.

CRXJS

The Vite plugin that understands Chrome extension structure. Created by Jack and Amy, it reads your manifest.json and handles content scripts, background workers, popup pages, and HMR automatically. We are contributors to this project (3.9k+ GitHub stars) and use it in every extension we ship. As Vite plugin authors ourselves, we understand the plugin API deeply and can extend the build pipeline when needed.

React

Component architecture translates perfectly to extensions. Popup UIs, option pages, and injected content script UIs are all just React components. Our internal react-content-script-injector library makes injecting React components into any page trivial.

TypeScript

Chrome API types catch errors at build time, not in production. The Chrome extension API surface is large and has many subtle type constraints - TypeScript surfaces these before your users do.

TanStack Query

Industry-standard data caching and server state management. In extensions, efficient caching is critical - you need fast access to API data without hammering endpoints or blocking the UI. TanStack Query handles cache invalidation, background refetching, and stale-while-revalidate patterns out of the box, so extension UIs stay responsive and data stays fresh.

This stack gives us hot module replacement in content scripts (yes, you can see changes injected into a live page without refreshing), type-safe messaging between extension contexts, and a development experience that feels like building a normal React app.

Every extension we build is covered by end-to-end, integration, and unit tests. E2E tests verify the full user journey inside the browser, integration tests validate that content scripts, background workers, and popups communicate correctly, and unit tests lock down individual utilities and business logic. This layered approach catches regressions before they reach production.

3. Manifest V3: what actually changed

Manifest V3 (MV3) replaced V2 as the required format for Chrome extensions. Google has been sunsetting MV2, so all new extensions must use V3. Here are the changes that matter in practice:

Service Workers

Background pages are gone. Service workers are event-driven and can be terminated by the browser at any time. This means you cannot rely on long-running state in the background - use chrome.storage or chrome.alarms instead.

DeclarativeNetRequest

The old webRequest API (for modifying network requests) is replaced by a declarative rules system. This is more restrictive but more performant. For most business extensions, this is not a blocker.

Content Security Policy

MV3 has stricter CSP defaults. No more eval() or inline scripts in extension pages. React and Vite handle this out of the box, so this rarely affects our workflow.

The practical impact: if you are building new, MV3 is just how extensions work now - and we are MV3 native. Every extension we build starts on Manifest V3 from day one, not as a migration from V2. If you are migrating an older extension from MV2, the service worker transition is the biggest hurdle. We have done this migration for multiple clients and can help if you are stuck.

4. Content scripts and injecting React into pages

Content scripts are the most powerful part of Chrome extensions. They run in the context of web pages, meaning they can read and modify the DOM of any page the user visits (with the right permissions).

The challenge is injecting a full React component tree into an existing page without breaking it. This is exactly what our internal react-content-script-injector library solves:

What react-content-script-injector does

  • -Monitors injection points and automatically re-mounts components when the host SPA re-renders or navigates
  • -Creates Shadow DOM containers with Tailwind CSS (or any styles) injected into the shadow root
  • -Reports errors in real time across different users - feature flags and A/B tests on the host site never silently break your extension
  • -Handles cleanup and re-injection on client-side route changes automatically
  • -Provides positioning utilities (sidebar, floating panel, inline)
  • -Handles React Portals so that the full React context (providers, state, theme) is always available - even inside Shadow DOM containers or injected panels

Static sites are the easy case - inject once and you are done. The hard part is React and SPA apps where the DOM is constantly changing, routes shift without page reloads, and the host app might be running A/B tests that move or rename the elements you are targeting. This is where we specialize, and why we built this library.

5. Shadow DOM: style isolation that works

Style isolation is the single biggest pain point in content script development. Without it, your extension UI inherits the host page's styles - buttons look wrong, fonts change, spacing breaks. Conversely, your extension's CSS can accidentally restyle the host page.

Shadow DOM provides true encapsulation. Styles inside the shadow root do not leak out, and styles outside do not leak in. This is critical for production extensions that need to work across thousands of different websites with different CSS resets, frameworks, and conflicting class names.

Our react-content-script-injector handles Shadow DOM setup automatically, including the tricky parts: injecting Tailwind into the shadow root, handling font loading across the shadow boundary, and making portals (modals, dropdowns) work correctly within the shadow context. Because the library manages React Portals natively, the full React context - providers, state, theme - stays available inside every portal, so components rendered in Shadow DOM behave exactly like components in a normal React tree.

6. Messaging between contexts

A Chrome extension has multiple isolated JavaScript contexts: the content script (runs on pages), the background service worker, the popup, and options pages. They communicate via the Chrome messaging API.

In practice, you end up with a pattern similar to a client-server architecture: the service worker is your "server" (handling API calls, storage, and business logic), and content scripts / popup are your "clients" that request data and trigger actions.

Our messaging best practices

  • 1.Define typed message schemas (TypeScript discriminated unions work well)
  • 2.Keep business logic in the service worker, UI logic in content scripts
  • 3.Use chrome.runtime.sendMessage for request/response patterns
  • 4.Use chrome.runtime.connect for long-lived connections (streaming data)
  • 5.Always handle the case where the service worker is not yet initialized

With TypeScript, you can make messaging fully type-safe: a message of type "GET_PRICES" always returns a typed PriceData response. This eliminates an entire class of runtime errors that are hard to debug across contexts.

7. Storage and state management

Extensions have their own storage APIs: chrome.storage.local for device-local data and chrome.storage.sync for data that follows the user across devices. Unlike localStorage, these are available in all extension contexts (content scripts, service worker, popup).

For complex state, we use a lightweight reactive store pattern: the service worker holds the source of truth, and content scripts subscribe to changes via chrome.storage.onChanged. This gives you real-time sync across all open tabs without any additional infrastructure.

One important caveat: chrome.storage.sync has strict size limits (102,400 bytes total, 8,192 bytes per item). For larger datasets, use chrome.storage.local and handle cross-device sync yourself if needed.

8. Deployment and distribution

There are four main distribution channels for Chrome extensions:

Chrome Web Store (public)

The standard distribution channel. Requires a Google developer account ($5 one-time fee), review process (usually 1-3 business days), and adherence to Chrome Web Store policies. Good for tools with a broad audience.

Chrome Web Store (unlisted)

Same store, but the extension is not discoverable via search. Users need a direct link. Perfect for internal tools and client-specific extensions. Still goes through review.

Enterprise sideloading

For organizations with Google Workspace or managed Chrome browsers. Extensions can be force-installed via group policy without going through the store. No review required, instant updates.

Automatic tester installs from GitHub

We build custom installer apps that automatically push new extension versions to QA testers' machines. Every push triggers a CI/CD build, and our installer picks up the latest artifact from GitHub and updates the extension on the tester's browser automatically - no manual downloads, no dragging files, no store review. QA always runs the newest build without lifting a finger, shortening the feedback loop from days to minutes.

We handle the full deployment pipeline: build, package, submit, and iterate on review feedback. Our CI/CD automatically bumps extension and package versions, builds the artifact, and deploys directly to the Chrome Web Store - so every merge to main is a potential release. For testers, the same pipeline publishes installable builds to GitHub. For enterprise clients, we add group policy distribution on top.

9. Common mistakes we have seen

After building and reviewing dozens of extensions, these are the patterns that cause the most problems:

Storing state in the background page

With MV3 service workers, your background context can be killed at any time. Any in-memory state is lost. Always persist important state to chrome.storage.

Not handling SPA navigation

Many modern web apps use client-side routing. Your content script is injected once on page load, but the URL can change without a full reload. Use MutationObserver or the webNavigation API to detect these transitions.

Requesting too many permissions

Users see the permissions list before installing. Requesting "access to all websites" when you only need access to one domain will tank your install rate. Use the minimum permissions required and consider optional permissions for advanced features.

Ignoring the Chrome Web Store review process

Google reviews every extension update. If you use remote code execution, excessive permissions, or patterns that look like data collection, your update will be rejected. Know the policies before you build.

No style isolation in content scripts

Without Shadow DOM or equivalent isolation, your extension will look different on every website. We have seen extensions that work perfectly on one site and are completely broken on another because of CSS conflicts.

Need help building your extension?

We are CRXJS contributors with 100+ production extension versions shipped. Whether you need a new extension built from scratch or help migrating to Manifest V3, we can help.

Get in touch

Have a project in mind? Let's discuss how we can help.

Or book a call directly: