Updated
—
4 min read
Using CSS modules with the shadow DOM may seem counterintuitive or redundant because they address the same problem of encapsulating styles.
After all, CSS modules are designed to facilitate component-based styling: each style is scoped to a specific component to prevent style conflicts.
NoteIf you find yourself trying to use Shadow DOM with CSS modules, first ask yourself if you really need both.
There are some valid reasons to use CSS modules + Shadow DOM, such as:
#section p { // these styles would have higher specificity }
. In this case, using Shadow DOM would provide additional style encapsulation to cover these edge cases.Whatever your reason is for using CSS modules and Shadow DOM, here's how to get them to play nice together.
Here's my quick (albeit somewhat hacky) way of easily getting CSS modules to work with Shadow DOM in React.
First, we'll attach a shadow root and pass it to our main <App />
React entry point component.
javascriptconst root = document.getElementById("root");
// Create a shadow root
const shadowRoot = root!.attachShadow({ mode: "open" });
// Create a container for your React app inside the shadow DOM
const reactContainer = document.createElement("div");
shadowRoot.appendChild(reactContainer);
const root = createRoot(reactContainer);
root.render(
<App
shadowRoot={shadowRoot} // Pass shadowRoot to App
/>
);
Then in our App.tsx
, which is a React functional component, we'll use a useEffect
to copy the style-loader
-injected style tags into our Shadow DOM.
javascript
const App: React.FC<{ shadowRoot: ShadowRoot }> = ({ shadowRoot }) => {
useEffect(() => {
const styles = document.querySelectorAll("style");
styles.forEach((style) => {
const clonedStyle = style.cloneNode(
true
) as HTMLStyleElement;
shadowRoot.appendChild(clonedStyle);
});
}, [shadowRoot]);
return (
<div>
{/* Rest of your React app */}
</div>
);
}
Here we are copying all the <style>
tags into our Shadow DOM.
Since we're using style-loader
, our CSS modules are injected into the DOM as <style>
tags.
However, you can also target only style tags created by style-loader
, like so:
javascriptconst styles = document.querySelectorAll("style[data-my-app-styles]")
And then in our Webpack config, we'll make style-loader
add the data-my-app-styles
attribute to each <style>
tag it injects.
Here's our basic Webpack setup for getting this to work.
javascriptconst path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const Dotenv = require('dotenv-webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
include: path.resolve(__dirname, 'src'),
use: 'ts-loader',
},
{
test: /\.module\.css$/,
use: [
{
loader: 'style-loader',
options: {
attributes: {
'data-my-app-styles': true,
},
},
},
],
},
{
test: /\.css$/,
exclude: /\.module\.css$/,
use: [
{
loader: 'style-loader',
options: {
attributes: {
'data-my-app-styles': true,
},
},
},
],
},
],
},
resolve: {
extensions: ['.ts', '.tsx'],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new Dotenv(),
],
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
port: 3000,
},
};
By adding a data attribute to our <style>
tags, we can update our useEffect
to only inject the style tags for our React app:
javascriptuseEffect(() => {
const styles = document.querySelectorAll("style[data-my-app-styles]");
styles.forEach((style) => {
const clonedStyle = style.cloneNode(
true
) as HTMLStyleElement;
shadowRoot.appendChild(clonedStyle);
});
}, [shadowRoot]);
To summarize, we copy <style>
tags into our Shadow DOM and add a data attribute via our Webpack config to only copy the style-loader
style tags we want.
While CSS modules and Shadow DOM may be unconventional and rarely used together (for good reason), hopefully this provides a simple, albeit somewhat hacky, way of ensuring your Shadow DOM can utilize existing CSS modules.
Meet the Author
Ryan Chiang
Hello, I'm Ryan. I build things and write about them. This is my blog of my learnings, tutorials, and whatever else I feel like writing about.
See what I'm building →.
Thanks for reading! If you want a heads up when I write a new blog post, you can subscribe below: