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

Lokman Musliu Founder and CEO of Lucky Media
Lokman Musliu

September 13, 2024 · 8 min read

How to create a chrome extension with React, Typescript, Tailwindcss, and Vite

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-ts

This 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 typescript

Configuring 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 -p

Setting 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;
React and Vite interaction

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.tsx component are in the App.tsx component.

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.

React 19 logo

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/chrome

Create 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 contexts

  • chrome.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. The chrome global 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 style attributes or <style> tags injected by JavaScript at runtime.

  • Extension not updating after code change - After running npm run build, go to chrome://extensions and click the reload icon on your extension. During development with npm 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 matches pattern in your manifest. "<all_urls>" matches all HTTP and HTTPS pages but not chrome:// or file:// 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 matches pattern. Wrap your chrome.tabs.sendMessage call 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.storage for 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

ReactTailwindCSSVite
Lokman Musliu Founder and CEO of Lucky Media
Lokman Musliu

Founder and CEO of Lucky Media

Stay up-to-date

Be updated with all news, products and tips we share!

Let’s chat

We partner with a limited number of brands each quarter to ensure senior-level attention on every project.

lokman and arlind headshots
Teamwork