
Building Chrome Extensions with React and Vite: A Complete Beginner's Guide
Learn how to build modern Chrome Extensions using React and Vite. Step-by-step guide covering setup, manifest V3, popup UI, background scripts, content scripts, and best practices.
Chrome Extensions let you add powerful features to the browser, and React makes building rich UIs easier than ever. But why Vite over Create React App? Vite is faster, lighter, and perfect for extensions—it builds quickly, outputs clean bundles, and handles hot reload smoothly. In this guide, you'll learn how to set up a React + Vite project for Chrome Extensions, structure your files correctly, create popup UIs, connect background and content scripts, handle Manifest V3, and ship your extension. We'll include ready-to-use code snippets and practical tips to avoid common pitfalls.
Why React + Vite for Chrome Extensions?
React gives you component-based UI development, state management, and a huge ecosystem. Vite offers instant dev server startup, fast HMR (Hot Module Replacement), and optimized production builds. Unlike Create React App, Vite doesn't bundle everything upfront—it serves source files directly in dev and only bundles what's needed. This means faster builds, smaller output, and better performance for extensions that need to load quickly.
Extensions have unique constraints: popups must open instantly, background scripts need to be lightweight, and content scripts run in isolated contexts. Vite's build output is clean and predictable, making it easier to configure manifest.json paths. Plus, Vite's plugin system lets you customize the build for extension-specific needs.
- Faster development: Instant server start and HMR
- Smaller bundles: Only what you need gets bundled
- Better for extensions: Clean output structure
- Modern tooling: ES modules, TypeScript support out of the box
Setting up your React + Vite project
Start by creating a new Vite project with React. Open your terminal and run: `npm create vite@latest my-extension -- --template react` (or use TypeScript: `--template react-ts`). Navigate into the project folder and install dependencies: `npm install`.
Next, install Chrome Extension-specific dependencies if needed. For this guide, we'll keep it simple and use React's built-in features. Your project structure should look like this:
- `src/` - Your React components and app code
- `public/` - Static assets (icons, manifest.json will go here)
- `vite.config.js` - Vite configuration
- `package.json` - Dependencies and scripts
Configuring Vite for Chrome Extensions
Chrome Extensions need multiple entry points: popup.html, background script, content scripts, and optionally an options page. Update your `vite.config.js` to build multiple outputs. Here's a basic configuration:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
input: {
popup: resolve(__dirname, 'popup.html'),
background: resolve(__dirname, 'src/background/background.ts'),
content: resolve(__dirname, 'src/content/content.ts'),
},
output: {
entryFileNames: (chunkInfo) => {
return chunkInfo.name === 'background' || chunkInfo.name === 'content'
? '[name].js'
: 'assets/[name]-[hash].js';
},
},
},
},
});Creating manifest.json
Create `public/manifest.json` with Manifest V3 structure. This file tells Chrome what your extension does, what permissions it needs, and where your files are. Here's a starter manifest:
Key points: `manifest_version: 3` is required, `action` replaces `browser_action` in V3, `background.service_worker` replaces background pages, and `type: "module"` lets you use ES modules.
{
"manifest_version": 3,
"name": "My React Extension",
"version": "1.0.0",
"description": "A Chrome Extension built with React and Vite",
"permissions": ["storage", "activeTab"],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
}
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
]
}Building your popup UI with React
Create `src/popup/Popup.tsx` as your main popup component. This is what users see when they click your extension icon. Keep it lightweight—popups should open instantly. Here's a simple example:
Create `public/popup.html` that loads your React app. In `src/popup/main.tsx`, render your Popup component into the root div.
import { useState } from 'react';
function Popup() {
const [count, setCount] = useState(0);
return (
<div style={{ width: '300px', padding: '20px' }}>
<h1>My Extension</h1>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
export default Popup;<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/popup/main.tsx"></script>
</body>
</html>Connecting background scripts with React
Background scripts (service workers in V3) run independently and handle events, API calls, and background tasks. They can't use React directly since they're not DOM-based, but you can structure them cleanly. Create `src/background/background.ts`:
To communicate between popup and background, use `chrome.runtime.sendMessage` and `chrome.runtime.onMessage.addListener`. Here's how to set up messaging:
// src/background/background.ts
chrome.runtime.onInstalled.addListener(() => {
console.log('Extension installed');
});
chrome.action.onClicked.addListener((tab) => {
// Handle icon click if no popup
});
// Listen for messages from popup/content scripts
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'getData') {
sendResponse({ data: 'result' });
}
return true; // Keep channel open for async response
});// In your popup component
chrome.runtime.sendMessage(
{ action: 'getData' },
(response) => {
console.log('Response:', response);
}
);Creating content scripts
Content scripts run in the context of web pages and can access the DOM. They're separate from your React app but can be built with Vite. Create `src/content/content.ts`:
Content scripts can't directly access React components, but you can inject a React app into the page if needed. Use `document.createElement` to add a container, then mount React into it. Be careful with conflicts—use shadow DOM or unique class names.
// src/content/content.ts
// Inject into page
function injectScript() {
const script = document.createElement('script');
script.src = chrome.runtime.getURL('content.js');
document.head.appendChild(script);
}
// Listen for messages from popup/background
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'modifyPage') {
// Modify DOM here
document.body.style.backgroundColor = '#f0f0f0';
sendResponse({ success: true });
}
return true;
});Handling Manifest V3 updates
Manifest V3 brings important changes. Background pages are now service workers that can be terminated, so avoid global state—use `chrome.storage` instead. Replace `chrome.tabs.executeScript` with `chrome.scripting.executeScript`. For network requests, use `declarativeNetRequest` instead of `webRequest` blocking. Update your permissions: `host_permissions` replaces some `permissions`, and `optional_host_permissions` lets users grant access on demand.
Test your extension thoroughly. Service workers wake on events, so ensure your listeners are registered at the top level. Use `chrome.alarms` for recurring tasks instead of `setInterval`. If you need persistent state, store it in `chrome.storage.local` or `chrome.storage.sync`.
Building and loading your extension
Run `npm run build` to create production files in the `dist/` folder. Vite will output your popup.html, background.js, content.js, and other assets. Copy your `manifest.json` and icons to the `dist/` folder (or configure Vite to copy them automatically).
To load in Chrome: Open `chrome://extensions/`, enable "Developer mode", click "Load unpacked", and select your `dist/` folder. Your extension will appear in the list. Click the extension icon to test the popup, and check the background service worker in the extension's "service worker" link.
- Build: `npm run build`
- Load: Chrome → Extensions → Load unpacked → Select `dist/`
- Debug: Right-click extension → Inspect popup, or click "service worker" for background
- Reload: After code changes, click the reload icon on the extension card
Common pitfalls and optimization tips
Avoid importing heavy libraries in popup—they slow down opening. Use code splitting if needed. Don't store sensitive data in `chrome.storage.sync` without encryption. Content scripts run in isolated worlds, so they can't access page JavaScript variables directly—use `window.postMessage` for communication if needed.
Optimize bundle size: Use Vite's build analysis (`npm run build -- --mode analyze` if configured) to find large dependencies. Consider lazy loading for options pages. Keep background scripts minimal—they affect browser performance. Use `chrome.storage.session` for temporary data that doesn't need to persist.
- Keep popup lightweight: Lazy load heavy components
- Use chrome.storage wisely: sync for settings, local for cache
- Test on real sites: Content scripts behave differently on different pages
- Handle errors gracefully: Service workers can be terminated unexpectedly
- Optimize images: Use WebP and appropriate sizes for icons
Why React + Vite is ideal for modern extensions
React + Vite gives you the best of both worlds: React's component model for building UIs quickly, and Vite's speed for fast iteration. The build output is clean and predictable, making it easy to configure for Chrome's requirements. Hot reload works great during development, and production builds are optimized automatically.
Extensions built this way are maintainable, scalable, and performant. You can use TypeScript for type safety, modern CSS solutions like Tailwind, and the entire React ecosystem. As Chrome Extensions evolve, having a solid foundation with React and Vite will make future updates easier.
More from the blog
View all posts