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

0

7

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);

Caressa answered 8/1 at 6:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.