Add Tailwind styles to shadow DOM
The problem with Tailwind
Adding Tailwind classes to shadow DOM elements wonāt directly work even though Tailwind is set up for your project. The definition of Tailwind classes that you applied is present in a stylesheet, but that stylesheet is not accessible inside shadow DOM. Read more about shadow DOM in my other article.
However, thereās a workaround using the Twind library.
Twind is a small compiler that converts Tailwind utility classes into CSS at runtime.
- No build step required.
- Twind ships the compiler, not the CSS. This means unlimited styles and variants for one low fixed cost of ~17kB.
- Framework agnostic.
- Support for Tailwind v3 with Tailwind preflight enabled by default.
- Twind claims to be faster than most CSS-in-JS libraries.
- Escape hatch for arbitrary CSS
Letās do this!
Install Twind
# install core package
npm install @twind/core
# add tailwind support and autoprefix plugin
npm install @twind/preset-autoprefix @twind/preset-tailwind
Install construct-style-sheets-polyfill
Our solution will use Constructable Stylesheet Objects and shadowRoot.adoptedStyleSheets
Ā but it has aĀ limited browser support. It wonāt work on Safari and old versions of Firefox etc. at the moment (December 2022). So we will be usingĀ Constructible style sheets polyfill,Ā which offers a solution for all modern browsers and IE 11. No changes will occur in a browser that supports this feature by default.
Install construct-style-sheets-polyfill
:
npm i construct-style-sheets-polyfill
Configure Twind
Twind config file
create twind.config.js
file at the root of your project and paste the below code for Tailwind and autoprefix plugin support:
import { defineConfig } from "@twind/core";
import presetTailwind from "@twind/preset-tailwind";
import presetAutoprefix from "@twind/preset-autoprefix";
export default defineConfig({
presets: [presetAutoprefix(), presetTailwind(/* options */)]
/* config */
});
You can add Tailwind plugins as well https://twind.style/presets#official-presets
Configuration for Typescript
if youāre using Typescript, open tsconfig.json
and mention module
as CommonJS
to make imports work correctly:
{
"compilerOptions": {
"module": "CommonJS"
}
}
Use Twind
In your js/ts file, create a custom stylesheet, create its Twind instance, link the sheet target to the shadow root, and tell Twind to observe for Tailwind classes:
import { twind, cssom, observe, install } from "@twind/core";
// support shadowroot.adoptedStyleSheets in all browsers
import "construct-style-sheets-polyfill";
// mention right path for twind.config.js
import config from "./twind.config";
// Create separate CSSStyleSheet
const sheet = cssom(new CSSStyleSheet());
// Use sheet and config to create an twind instance. `tw` will
// append the right CSS to our custom stylesheet.
const tw = twind(config, sheet);
// get hold of the shadow dom root
const shadowRoot = document.querySelector(
".my-shadow-root-container"
).shadowRoot;
// link sheet target to shadow dom root
shadowRoot.adoptedStyleSheets = [sheet.target];
// finally, observe using tw function
observe(tw, shadowroot);
Thatās it; just write your friendly neighbourhood Tailwind classes, and Twind (tw
) will link its corresponding style to that custom stylesheet.
<div className="text-gray-700 bg-slate-50">Title</div>
Prevent parent page styles from leaking into shadow DOM
Shadow DOM can still inherit some css styles from parent page like base font size and line height but we can prevent that as well. First we need to use px
instead of rem
(Tailwind's default unit) for all css properties. Luckily thereās a way to convert unit during build step:
Add below to twind.config.js
:
//https://github.com/tw-in-js/twind/issues/437#issue-1532077112
const presetRemToPx = ({ baseValue = 16 } = {}) => {
return {
...rule,
// d: the CSS declaration body
// Based on https://github.com/TheDutchCoder/postcss-rem-to-px/blob/main/index.js
d: rule.d
? rule.d.replace(
/"[^"]+"|'[^']+'|url\([^)]+\)|(-?\d*\.?\d+)rem/g,
(match, p1) => {
if (p1 === undefined) return match;
return `${p1 * baseValue}${p1 == 0 ? "" : "px"}`;
}
)
: ""
};
}
};
};
export default defineConfig({
presets: [presetAutoprefix(), presetTailwind(/* options */), presetRemToPx()]
/* config */
});
Second, we need to set line-height
instead of relying upon parent pageās default line-height
. We can do it on the shadow DOM container element:
// set line-height to shadow dom container
document.querySelector(".my-shadow-root-container").style.lineHeight="18px";