How to use React and TailwindCSS in Chrome Extension Content Scripts

Recently, I've grown fond of Tailwind CSS's utility-first approach to writing CSS. Naturally, I wanted to incorporate it into browser extension development as well.

Everything was going smoothly until I encountered content scripts, where it all stopped working. Hence, this article aims to help solve this issue.

Problem description

Content scripts are JavaScript files that run in the context of web pages. They can read and modify the DOM of web pages the browser visits. However, they are isolated from the rest of the extension.

As a result, there will be some differences in how we want to use tailwindcss in our content scripts, as well as this issue

Style Isolation: The styles defined in the content script are not isolated from the styles of the original page. This can lead to conflicts and unintended side effects.

Solution

After research and experimentation, I've found an effective method to solve this problem:

1. Initialization project

I chose guocaoyi/create-chrome-ext for scaffolding. Although I haven't tried it, I believe that crxjs would also benefit from the below solution.

npx create-chrome-ext@latest
Initialization Project

2. Install dependencies

First, we need to install the scaffold dependencies:

npm install

Then install tailwindcss dependencies (Refer to this official document)

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init

3. Tailwindcss important configurations

After the initialization in the previous step, you'll get a tailwind.config.ts file, be sure to configure the content attribute in it

It is used to specify which files tailwindcss should look for class names in to generate the appropriate CSS.

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./*.html",
    "./src/**/*.{js,jsx,ts,tsx}"
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

4. Add Tailwind to your PostCSS configuration

Add tailwindcss and autoprefixer to your postcss.config.js file, or wherever PostCSS is configured in your project.

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}

5. Start injecting styles into content scripts

Create a file named index.css containing the basic TailwindCSS directives in the src/contentScript directory.

@tailwind base;
@tailwind components;
@tailwind utilities;

Change the name of the src/contentScript/index.ts file tosrc/contentScript/index.tsx, and import the index.css file in it.

import css from './index.css?inline'
...

...
createRoot(containerDomVar).render(
  <React.StrictMode>
    <style type="text/css">{css}</style>
    <App />
  </React.StrictMode>,
)

6. Enjoy tailwindcss in content scripts

  1. Start the development service with the command npm run dev
  2. Access chrome://extensions/
  3. Turn on developer mode
  4. Click 'load unpacked', and select build directory

Conclusion

By following these steps, we've successfully integrated React and TailwindCSS in a Chrome extension's content script, overcoming the challenges of style isolation and efficient CSS management in this unique environment.

index.tsx full example like below


import React from 'react'
import { createRoot } from 'react-dom/client'
// 1. Import the css file
import css from './index.css?inline'
// import appCss from './app.css?inline'

function App() {
  return (
    <>
      {/* You can also import a separate and isolated app css file like below,*/}
      {/* without affecting the css style of the original page */}
      {/* <style type="text/css">{appCss}</style> */}
      <h1 className="text-red-500">Hello world!</h1>
    </>
  )
}

const onLoaded = () => {
  const root = document.createElement('div')
  document.body.prepend(root)
  const shadowRoot = root.attachShadow({ mode: 'open' })

  const renderIn = document.createElement('div')
  shadowRoot.appendChild(renderIn)

  createRoot(renderIn).render(
    <React.StrictMode>
      // 2. Inject the css file
      <style type="text/css">{css}</style>
      <App />
    </React.StrictMode>,
  )
}

if (document.readyState === 'complete') {
  onLoaded()
} else {
  window.addEventListener('load', () => {
    onLoaded()
  })
}