How to Create a Chrome Extension with React, TypeScript, TailwindCSS, and Vite

September 13, 2024 · 8 min read

Creating a Chrome extension can be a fun and rewarding project, especially when you combine powerful tools like React, TypeScript, TailwindCSS, and Vite. In this article, we’ll walk you through the entire process step-by-step, ensuring you have a clear understanding of how to build your own Chrome extension in 2024. Whether you’re a seasoned developer or just starting out, this guide will help you navigate the complexities of extension development with ease.
Creating a React Chrome Extension
Have you ever thought about creating your own Chrome extension? Maybe you have a brilliant idea that could make browsing easier or more enjoyable. Let’s create a Chrome extension using modern web technologies: React for building user interfaces, TypeScript for type safety, TailwindCSS for styling, and Vite for a fast development experience. By the end of this article, you’ll have a fully functional extension and the knowledge to expand on it.
Setting Up Your Development Environment
Installing Node.js and npm
To get started, download and install Node.js from the official website. This will also install npm, which you’ll use to manage your project dependencies.
Creating a New Vite Project
Once Node.js is installed, open your terminal and run the following command to create a new Vite project:
# npm 7+, extra double-dash is needed:
npm create vite@latest my-chrome-extension -- --template react-tsThis command sets up a new project with React and TypeScript.
Understanding Chrome Extensions
Manifest File Overview
Every Chrome extension needs a manifest file (manifest.json). This file contains metadata about your extension, including its name, version, permissions, and the background scripts it will use.
Key Components of a Chrome Extension
A typical Chrome extension consists of:
Background scripts: Run in the background and handle events.
Content scripts: Injected into web pages to interact with the DOM.
Popup UI: The interface that appears when you click the extension icon.
Integrating React with Vite
Setting Up React in Vite
After creating your Vite project, navigate to your project directory and run npm install.
Creating Your First Component
Create a new component in the src folder, for example, Popup.tsx:
import React from 'react';
const Popup: React.FC = () => (
<div className="p-4">
<h1 className="text-lg font-bold">
Hello, Chrome Extension!
</h1>
</div>
);
export default Popup;Now in our App.tsx file we need to import our Popup.tsx component that we just created:
import Popup from "./Popup";
const App: React.FC = () => {
return <Popup />;
};
export default App;Adding TypeScript to Your Project
Installing TypeScript
If you choose the React + TypeScript template, TypeScript will already be installed. If not, you can add it with:
npm install --save-dev typescriptConfiguring TypeScript
Create a tsconfig.json file in your project root to configure TypeScript options. You can start with a basic configuration:
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}Styling with TailwindCSS
Installing TailwindCSS
To add TailwindCSS, run the following commands:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -pSetting Up TailwindCSS with Vite
In your tailwind.config.js, configure the paths to your template files:
module.exports = {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}'
],
theme: {
extend: {},
},
plugins: [],
};Then, include Tailwind in your CSS by adding the following lines to your src/index.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
Building Your Chrome Extension
Install the CRXJS Vite Plugin
To be able to bundle a Chrome Extension we need a plugin for Vite that will make our job a little bit easier, by handling things like HMR and static asset imports.
We can start by installing it with the command npm i @crxjs/vite-plugin@beta -D.
Update the Vite config
Update vite.config.ts to match the code below:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { crx } from '@crxjs/vite-plugin'
import manifest from './manifest.json'
export default defineConfig({
plugins: [
react(),
crx({ manifest }),
],
})Create a file named manifest.json next to vite.config.js:
{
"manifest_version": 3,
"name": "My Chrome Extension",
"version": "1.0.0",
"description": "A Chrome extension built with Vite and React",
"action": {
"default_popup": "index.html"
},
"permissions": []
}Testing Your Extension
Loading the Extension in Chrome
Now that you have everything ready, it’s time to give it a test run in the browser.
If you haven’t started Vite in the terminal, you can do it by running npm run dev.
By default, you should see a Popup when you click on the extension. The contents of that
Popup.tsxcomponent are in theApp.tsxcomponent.
To test your extension, open Chrome and navigate to chrome://extensions. Enable Developer mode and click Load unpacked. Select your project’s dist folder.
Debugging Tips
If something isn’t working, check the console for errors. You can access the console by right-clicking on your extension popup and selecting Inspect.
Publishing Your Extension
Preparing for Submission
Before publishing, ensure your extension meets the Chrome Web Store’s policies. You may need to create a promotional image and write a detailed description.
Publishing on the Chrome Web Store
Go to the Chrome Web Store Developer Dashboard, create a new item, and upload your extension package (the zip file of your project). Follow the prompts to complete the submission.

Manifest V3: What Changed and Why It Matters
Manifest V3 (MV3) is the current extension platform standard — all new extensions must use it, and Chrome has deprecated Manifest V2. If you are following tutorials from 2021 or earlier, you are likely reading MV2 examples which will not work correctly today.
Key changes in Manifest V3:
Background pages replaced by service workers - Background scripts no longer run persistently. They are event-driven and terminate when idle.
Stricter Content Security Policy - Inline scripts and eval() are blocked. This affects some bundler setups and CSS-in-JS libraries.
Declarative Net Request instead of webRequest - Ad blockers and request modifiers must use the new declarativeNetRequest API.
Permissions are more granular - Host permissions must be declared separately from API permissions.
Here is a complete manifest.json for an extension with a popup, content script, background service worker, and icons:
{
"manifest_version": 3,
"name": "My Chrome Extension",
"version": "1.0.0",
"description": "A Chrome extension built with React, TypeScript, and Vite",
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"action": {
"default_popup": "index.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png"
}
},
"background": {
"service_worker": "src/background/background.ts",
"type": "module"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["src/content/content.ts"]
}
],
"permissions": ["storage", "activeTab", "tabs"],
"host_permissions": ["<all_urls>"]
}For icons, you need four sizes: 16, 32, 48, and 128px. Create a public/icons/ folder and export your icon at each size from Figma, or use a tool like sharp to generate them automatically from a source PNG.
Adding a Content Script
A content script runs in the context of a web page — it can read and modify the DOM of any page the user visits. This is what makes Chrome extensions powerful: your code can interact with any website.
First, install the Chrome types so TypeScript knows about the chrome global:
npm install --save-dev @types/chromeCreate src/content/content.ts:
// src/content/content.ts
// This script runs in the context of every web page
console.log('Content script loaded on:', window.location.href);
// Example: highlight all links on the page
function highlightLinks() {
const links = document.querySelectorAll('a');
links.forEach((link) => {
link.style.backgroundColor = 'yellow';
});
}
// Listen for messages from the popup or background
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'highlight') {
highlightLinks();
sendResponse({ success: true, count: document.querySelectorAll('a').length });
}
});Content scripts are isolated from the page JavaScript — they share the DOM but not the JavaScript scope. They cannot access variables defined on the page, and the page cannot access variables in your content script.
Background Service Workers
The background service worker is your extension's event handler. It runs separately from any tab, handles long-running tasks, and coordinates between the popup and content scripts. Unlike the old background pages, service workers do not run persistently — they wake up to handle an event and go idle again.
// src/background/background.ts
// Fires once when the extension is installed or updated
chrome.runtime.onInstalled.addListener((details) => {
console.log('Extension installed:', details.reason);
// Set default storage values on first install
if (details.reason === 'install') {
chrome.storage.local.set({ enabled: true, highlightColor: 'yellow' });
}
});
// Listen for messages from content scripts or the popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'getTabUrl') {
// Get the URL of the active tab
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
sendResponse({ url: tabs[0]?.url });
});
return true; // return true to keep the message channel open for async response
}
});Important: Because service workers can be terminated at any time, you cannot store state in variables. Any data that needs to persist across events must be saved to chrome.storage.
Chrome Extension Messaging
The popup, content scripts, and background service worker are three separate JavaScript contexts. They communicate by passing messages using the Chrome Messaging API.
Sending a Message from the Popup
// src/Popup.tsx
'use client';
import { useState } from 'react';
const Popup: React.FC = () => {
const [count, setCount] = useState<number | null>(null);
const handleHighlight = async () => {
// Send a message to the content script in the active tab
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab.id) {
const response = await chrome.tabs.sendMessage(tab.id, {
action: 'highlight',
});
setCount(response.count);
}
};
return (
<div className="p-4 w-64">
<h1 className="text-lg font-bold mb-2">Link Highlighter</h1>
<button
onClick={handleHighlight}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
Highlight Links
</button>
{count !== null && (
<p className="mt-2 text-sm text-gray-600">{count} links highlighted</p>
)}
</div>
);
};
export default Popup;Sending a Message to the Background
// From popup or content script, send to background service worker:
chrome.runtime.sendMessage({ action: 'getTabUrl' }, (response) => {
console.log('Current tab URL:', response.url);
});Persisting Data with the Chrome Storage API
Do not use localStorage in Chrome extensions. It is not accessible from background service workers, it is not synced across devices, and data does not persist reliably between extension contexts. Use chrome.storage instead.
chrome.storage.local- stored on device, large quota (10MB by default), accessible from all extension contextschrome.storage.sync- synced across devices via Chrome account, smaller quota (100KB), ideal for user preferences
// Writing to storage
await chrome.storage.local.set({ enabled: true, color: 'yellow' });
// Reading from storage
const result = await chrome.storage.local.get(['enabled', 'color']);
console.log(result.enabled); // true
console.log(result.color); // 'yellow'
// Removing a key
await chrome.storage.local.remove('color');
// Listen for changes
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'local' && changes.enabled) {
console.log('enabled changed to:', changes.enabled.newValue);
}
});Here is a reusable React hook that wraps Chrome Storage for use in your popup components:
// src/hooks/useChromeStorage.ts
import { useState, useEffect } from 'react';
export function useChromeStorage<T>(key: string, defaultValue: T) {
const [value, setValue] = useState<T>(defaultValue);
useEffect(() => {
chrome.storage.local.get([key], (result) => {
if (result[key] !== undefined) {
setValue(result[key] as T);
}
});
const listener = (changes: { [key: string]: chrome.storage.StorageChange }) => {
if (changes[key]) {
setValue(changes[key].newValue as T);
}
};
chrome.storage.onChanged.addListener(listener);
return () => chrome.storage.onChanged.removeListener(listener);
}, [key]);
const setStoredValue = (newValue: T) => {
chrome.storage.local.set({ [key]: newValue });
};
return [value, setStoredValue] as const;
}Usage in your popup component:
import { useChromeStorage } from './hooks/useChromeStorage';
const Popup: React.FC = () => {
const [enabled, setEnabled] = useChromeStorage('enabled', false);
return (
<div className="p-4 w-64">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={enabled}
onChange={(e) => setEnabled(e.target.checked)}
/>
Enable extension
</label>
</div>
);
};Common Errors and Troubleshooting
"Cannot find name chrome" or "chrome is not defined" - Install the Chrome types package:
npm install --save-dev @types/chrome. Thechromeglobal is not typed by default in TypeScript."Refused to execute inline script" (CSP error) - Manifest V3 blocks inline scripts. If using TailwindCSS v3, ensure you are using a PostCSS build (which this guide does). Avoid any
styleattributes or<style>tags injected by JavaScript at runtime.Extension not updating after code change - After running
npm run build, go tochrome://extensionsand click the reload icon on your extension. During development withnpm run dev, CRXJS handles HMR automatically for the popup — but content scripts and background workers may require a manual reload.Content script not running on a page - Check the
matchespattern in your manifest."<all_urls>"matches all HTTP and HTTPS pages but notchrome://orfile://URLs."Could not establish connection. Receiving end does not exist" - The content script has not loaded on the current tab yet, or the tab URL does not match the
matchespattern. Wrap yourchrome.tabs.sendMessagecall in a try/catch.Background service worker stops working after idle - Service workers terminate after ~30 seconds of inactivity. This is expected behaviour in MV3. Do not rely on in-memory state persisting between events. Use
chrome.storagefor anything that needs to survive termination.
Conclusion
Creating a Chrome extension with React, TypeScript, TailwindCSS, and Vite is a great way to enhance your development skills. The popup is just the starting point - with content scripts, background service workers, the messaging API, and Chrome Storage, you can build extensions that interact with any page, persist user preferences, and coordinate complex workflows across browser contexts. Enjoy your extension and keep experimenting.
FAQs
Can I use other frameworks instead of React?
Yes, you can use any JavaScript framework or library, such as Vue or Angular, to build your Chrome extension.
Is it necessary to use TypeScript?
No, but TypeScript provides type safety and can help catch errors early in the development process.
How do I update my extension after publishing?
You can update your extension by incrementing the version number in the manifest file and re-uploading the package to the Chrome Web Store.
Can I monetize my Chrome extension?
Yes, you can monetize your extension through various methods, such as offering premium features or displaying ads.
What are some common mistakes to avoid when creating a Chrome extension?
Avoid overcomplicating your extension, neglecting user privacy, and failing to test thoroughly before publishing.
Bring Your Ideas to Life 🚀
If you need help with a React project let’s get in touch.
Lucky Media is proud to be recognized as a leading Next.js Development Agency
Technologies

Stay up-to-date
Be updated with all news, products and tips we share!
On this page
- Creating a React Chrome Extension
- Setting Up Your Development Environment
- Understanding Chrome Extensions
- Integrating React with Vite
- Adding TypeScript to Your Project
- Styling with TailwindCSS
- Building Your Chrome Extension
- Testing Your Extension
- Publishing Your Extension
- Manifest V3: What Changed and Why It Matters
- Adding a Content Script
- Background Service Workers
- Chrome Extension Messaging
- Persisting Data with the Chrome Storage API
- Common Errors and Troubleshooting
- Conclusion
- FAQs
- Bring Your Ideas to Life 🚀

