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