Let AI write emails and messages for you 🔥

Add Tailwind styles to shadow DOM

Gourav Goyal

Gourav Goyal

Dec 15, 2022

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";
That's all, folks!

Gourav Goyal

Gourav Goyal