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.
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
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);
});
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);
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.
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.
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.
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**/
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With