Skip to content

How to build a Chrome Extension using React, Tailwind and Vite

In this article, we will learn how to build a Chrome Extension using React, Tailwind CSS and Vite.

Prerequisites

Before we start building the Chrome Extension, make sure you have the following tools installed on your machine:

Create and Initialize a new React app

npm create vite@latest my-chrome-extension -- --template react-ts
cd my-chrome-extension
npm install

Install & Configure Tailwind CSS

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
@tailwind base;
@tailwind components;
@tailwind utilities;

Configure Vite for Chrome extension

Create a vite.config.ts file in the root directory:

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, 'index.html'),
        background: resolve(__dirname, 'src/background.ts'),
        content: resolve(__dirname, 'src/content.ts')
      },
      output: {
        entryFileNames: 'assets/[name].js',
        chunkFileNames: 'assets/[name].js',
        assetFileNames: 'assets/[name].[ext]'
      }
    }
  }
})

Create manifest.json

Create a manifest.json file in the root directory:

{
"manifest_version": 3,
"name": "My Chrome Extension",
"version": "1.0.0",
"description": "A Chrome extension built with React, Vite, TypeScript, and Tailwind",
"action": {
  "default_popup": "index.html",
  "default_icon": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  }
},
"permissions": [],
"background": {
  "service_worker": "assets/background.js",
  "type": "module"
},
"content_scripts": [
  {
    "matches": ["<all_urls>"],
    "js": ["assets/content.js"]
  }
],
"icons": {
  "16": "icons/icon16.png",
  "48": "icons/icon48.png",
  "128": "icons/icon128.png"
}
}

Create a background.js file

Create a new file src/background.ts:

console.log('Background script running');

// Listen for messages from the popup or content scripts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  console.log('Received message:', message);
  sendResponse({ status: 'Message received' });
  return true;
});

Create a content.js file

Create a new file src/content.ts:

console.log('Content script loaded');

// Example: Send a message to the background script
chrome.runtime.sendMessage({ action: 'contentScriptLoaded' }, (response) => {
  console.log('Response:', response);
});

Create a Popup Component

Create a new file src/Popup.tsx:

import React from 'react'

export default function Popup() {

  const sendMessageToBackground = () => {
    chrome.runtime.sendMessage({ action: 'popupAction', data: 'Hello from popup!' }, 
      (response) => {
        console.log('Response from background:', response);
      }
    );
  };

  return (
    <div className="p-4 bg-white shadow rounded-lg">
      <h1 className="text-xl font-bold">Hello, Chrome Extension!</h1>
      <button 
        className="mt-2 px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
        onClick={sendMessageToBackground}
      >
        Send Message
      </button>
    </div>
  )
}

Update index.tsx

Update src/index.tsx to render the Popup component:


import React from 'react'
import ReactDOM from 'react-dom'
import Popup from './Popup'

ReactDOM.render(<Popup />, document.getElementById('root'))

Use chrome.storage

Create a new file src/storage.ts:

export const Storage = {
  async set<T>(key: string, value: T): Promise<void> {
    const data = JSON.stringify(value);
    if (chrome.storage) {
      await chrome.storage.local.set({ [key]: data });
    } else {
      localStorage.setItem(key, data);
    }
  },

  async get<T>(key: string, defaultValue: T): Promise<T> {
    if (chrome.storage) {
      return new Promise<T>((resolve) => {
        chrome.storage.local.get([key], (result) => {
          resolve(result[key] ? JSON.parse(result[key]) : defaultValue);
        });
      });
    } else {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : defaultValue;
    }
  },

  async remove(key: string): Promise<void> {
    if (chrome.storage) {
      await chrome.storage.local.remove(key);
    } else {
      localStorage.removeItem(key);
    }
  },

  async clear(): Promise<void> {
    if (chrome.storage) {
      await chrome.storage.local.clear();
    } else {
      localStorage.clear();
    }
  },
};

Example of usage:

import { Storage } from './storage';

Storage.set('myKey', { name: 'John' });
const data = await Storage.get('myKey', { name: 'John' });

Install crx-js Vite Plugin (Optional)

CRX is a Vite plugin that allows you to build Chrome extensions. Install it using npm:

npm install --save-dev @crxjs/vite-plugin@beta

Build the Chrome Extension

Run the following command to build the Chrome Extension:

npm run build

Load the Chrome Extension

Load the extension in Chrome:

  1. Open Chrome and go to chrome://extensions/
  2. Enable "Developer mode"
  3. Click "Load unpacked" and select the dist folder in your project directory

Publish the Chrome Extension

  1. Go to the Chrome Web Store Developer Dashboard
  2. Click "Create new item" and select "Chrome extension"
  3. Fill in the required information
  4. Upload the extension files
  5. Click "Publish"