Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to inject tailwind classes into shadow element so React Component gets correctly styled?

The context: I am building a chrome extension using React, Typescript, Tailwind, and Craco. Tailwind classes get applied correctly in the chrome extension popup, but I also want to be able to add React components onto a webpage via content scripts. (I am using example.com to test my component injections)

The Problem: I am rendering a shadow-root (in order to isolate any styling from rest of the page) dynamically and rendering the React Component inside the shadow-root. The component's html structure and js are working correctly, but the applied tailwind is not being rendered, even though they are referenced in the component.

// Test.tsx
import React, { FC, useState } from "react";

const Test: FC = () => {
  const [msg, setMsg] = useState<string>("");
  
  return (
    <div className="bg-purple-300" onClick={() => setMsg("i have been clicked")}>
      {msg.trim().length > 0 ? <p>{msg}</p> : <p className="italic">TEST TEST TEST TEST</p>}
    </div>
  )
}

export default Test;
//content.ts
const shadowRoot = document.createElement('div').attachShadow({ mode: 'open' });
const tailwindSheet = new CSSStyleSheet();
tailwindSheet.replaceSync(`
  .bg-purple-300 {
    background-color: #d8b5e5;
  }
`);
shadowRoot.adoptedStyleSheets = [tailwindSheet];
ReactDOM.render(React.createElement(Test), shadowRoot);
document.body.appendChild(shadowRoot.host);

I'm not sure if it matters but:

//craco.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
  webpack: {
    configure: (webpackConfig, { env, paths }) => {
      return {
        ...webpackConfig,
        entry: {
          main: [
            env === "development" &&
              require.resolve("react-dev-utils/webpackHotDevClient"),
            paths.appIndexJs,
          ].filter(Boolean),
          background: paths.appSrc + "/scripts/background.ts",
          content: paths.appSrc + "/scripts/content.ts"
        },
        output: {
          ...webpackConfig.output,
          filename: "static/js/[name].js",
        },
        optimization: {
          ...webpackConfig.optimization,
          runtimeChunk: false,
        },
        plugins: [
          ...webpackConfig.plugins,
          new HtmlWebpackPlugin({
            inject: true,
            chunks: ["options"],
            template: paths.appHtml,
            filename: "options.html",
          }),
        ],
      };
    },
  },
};

The manifest.json:

{
  "name": "Custom Chrome Extension",
  "description": "Template for creating Crome extensions with React",
  "version": "1.0",
  "manifest_version": 3,
  "action": {
      "default_popup": "index.html",
      "default_title": "Open the popup"
  },
  "icons": {
      "16": "logo192.png",
      "48": "logo192.png",
      "128": "logo192.png"
  },
  "permissions": ["activeTab", "scripting"],
  "host_permissions": ["https://*/*", "http://*/*"],
  "background": {
    "service_worker": "static/js/background.js",
    "type": "module"
  },
  "content_scripts": [
    { 
      "matches": ["https://*/*", "http://*/*"],
      "js": ["static/js/content.js"]
    }
  ]
}

I understand that the problem is that tailwind utility classes need to be added to the shadow-root so that the classes are accessible from the injected components.

  1. I tried adding a script tag with the tailwind cdn:
const shadowRoot = document.createElement('div').attachShadow({ mode: 'open' });
const twScript = document.createElement('script');
twScript.src = "https://cdn.tailwindcss.com";
shadowRoot.appendChild(twScript);
ReactDOM.render(React.createElement(Test), shadowRoot);
document.body.appendChild(shadowRoot.host);

The script element is never rendered, I think maybe because it violates content security policy

  1. Another thought I've had is to try and use fetch to get raw tailwind class definitions as a giant string, then use it as the argument to replaceSync(), but I wasn't able to find a website to fetch from.
const shadowRoot = document.createElement('div').attachShadow({ mode: 'open' });
fetch('magicurlthatgivesmetailwindcss').then(response => response.text()).then(cssText => {
  const tailwindSheet = new CSSStyleSheet();
  tailwindSheet.replaceSync(cssText);
  shadowRoot.adoptedStyleSheets = [tailwindSheet];
  ReactDOM.render(React.createElement(Test), shadowRoot);
  document.body.appendChild(shadowRoot.host);
});
  1. My last idea is to somehow extract the name of the tailwind classes I used in my react components before I render them, then manually generate a string with the css class definitions. Then I can use the string as an argument to replaceSync() to preload the raw css. I don't even know how I would begin going about this, or if it's possible.

So far, replaceSync() has been the only successful way to get any CSS styling on the webpage:

const shadowRoot = document.createElement('div').attachShadow({ mode: 'open' });
const tailwindSheet = new CSSStyleSheet();
tailwindSheet.replaceSync(`
  .bg-purple-300 {
    background-color: #d8b5e5;
  }
`);
shadowRoot.adoptedStyleSheets = [tailwindSheet];
ReactDOM.render(React.createElement(Test), shadowRoot);
document.body.appendChild(shadowRoot.host);

like image 261
Felix Heox Avatar asked Jun 01 '26 03:06

Felix Heox


1 Answers

I've faced the same problem because of shadcnui, I'm using Vite and React but I'm sure this is not why you are facing this problem.

Rendering content with Vite and React

You can see my code to render content:


import browser from 'webextension-polyfill';

export default async function renderContent(
  cssPaths: string[],
  render: (appRoot: HTMLElement) => void
) {
  const appContainer = document.createElement('div');
  const shadowRoot = appContainer.attachShadow({
    mode: import.meta.env.DEV ? 'open' : 'closed',
  });
  const appRoot = document.createElement('div');
  if (import.meta.hot) {
    const { addViteStyleTarget } = await import(
      '@samrum/vite-plugin-web-extension/client'
    );

    await addViteStyleTarget(shadowRoot);
  } else {
    cssPaths.forEach((cssPath: string) => {
      const styleEl = document.createElement('link');
      styleEl.setAttribute('rel', 'stylesheet');
      styleEl.setAttribute('href', browser.runtime.getURL(cssPath));
      shadowRoot.appendChild(styleEl);
    });
  }

  shadowRoot.appendChild(appRoot);
  document.body.appendChild(appContainer);

  render(appRoot);
}

This code uses the webextension-polyfill package to use standardized API between browsers.

As you can see, this code creates a shadow root and injects the CSS-paths as links into the shadow root. Pretty much like your solutions right? So you are on the right path.

Using the renderContent functionality

import React from 'react';
import ReactDOM from 'react-dom/client';
import renderContent from '../renderContent';
import App from './App';

renderContent(import.meta.PLUGIN_WEB_EXT_CHUNK_CSS_PATHS, (appRoot) => {
  ReactDOM.createRoot(appRoot).render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
});

The real magic here is how you can get those paths since I'm using Vite, I can use import.meta.PLUGIN_WEB_EXT_CHUNK_CSS_PATHS thanks to @samrum/vite-plugin-web-extension, which is a plugin for Vite that looks for all occurrences of the import.meta.PLUGIN_WEB_EXT_CHUNK_CSS_PATHS and pass all paths of my CSS. This will make sure that your tailwind CSS class works properly.

Using CSS Variables or ShadcnUI

if you want to use CSS variables to theme your extension like shadcnui does, YOU CAN'T USE THE :root DIRECTIVE!

The reason is really cool to know and it's related to how the whole Cascading Style Sheet works. You can see another topic related to learn more. The thing is, you can use the :host to use the style encapsulation method, or you can also use a unique id for that.

@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base{
 :host{
  --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.9%;
    /**other CSS variables that you want**/

 }
}
like image 181
Henrique de Paula Rodrigues Avatar answered Jun 02 '26 20:06

Henrique de Paula Rodrigues