tailwindcss icon indicating copy to clipboard operation
tailwindcss copied to clipboard

@property isn't supported in shadow roots

Open blittle opened this issue 1 year ago • 29 comments

What version of Tailwind CSS are you using?

For example: 4.0.0-alpha.34

What build tool (or framework if it abstracts the build tool) are you using?

Web components with shadow roots.

What version of Node.js are you using?

v22.2.0

What browser are you using?

Chrome

What operating system are you using?

macOS

Reproduction URL

https://github.com/blittle/tw-shadow

Describe your issue

Tailwind v4 uses @property to define defaults for custom properties. At the moment, shadow roots do not support @property. It used to be explicitly denied in the spec, but it looks like there's talk on adding it: https://github.com/w3c/css-houdini-drafts/pull/1085

I don't know if this is something tailwind should fix, but it took me a while to find the issue, so it's probably worth keeping this issue to document the limitation.

Here is a work-around, just attaching the @property definitions to the base document. It would be nice if tailwind provided an easy way to import just that content.

An easy way to do that with Vite is to create a tailwind css file specifically for the properties and apply a transform:

export default defineConfig(() => {
  return {
    ...
    plugins: [
      tailwindcss(),
      {
        name: "tailwind-properties",
        transform(code, id) {
          if (id.endsWith("tailwind-properties.css?inline")) {
            // Change custom properties to inherit
            code = code.replaceAll("inherits: false", "inherits: true");

            // Remove everything before the property declarations
            code = code.substring(code.indexOf("@property"));

            return code;
          }
        },
      },
    ],
  };
});

blittle avatar Nov 14 '24 20:11 blittle

Not 100% sure if this is related, but I noticed that in a shadow root, the defaults under @theme aren't available. I.e. I can't see --spacing in devtools, and it doesn't have any value.

Essentially rendering Tailwind unusable in shadow roots for most use cases.

ErwinAI avatar Nov 22 '24 08:11 ErwinAI

Tailwind CSS v4’s @property rules aren't supported in shadow roots. A temporary fix involves defining these properties in a separate CSS file and using Vite to adjust them. Your Vite configuration snippet modifies inheritance settings and ensures proper implementation.

Great solution for handling this limitation!

rose21wiley avatar Nov 28 '24 08:11 rose21wiley

@blittle Thanks for the detailed writeup and sorry for the late answer here.

From what I understand after doing some testing, it is indeed still required that the @property definitions are hoisted to the base document.

The snippet you have for that also changes the inherits property. Do you remember why this was necessary? I was playing around it in a simple playground and found that this wasn't necessary for my tests. I didn't transform the sources via Vite though but added this snippet directly within the runtime of a custom component's connectedCallback hook:

let atProperties = styles.slice(styles.indexOf('@property'))
let style = document.createElement('style')
style.innerText = atProperties
document.head.appendChild(style)

philipp-spiess avatar Jan 29 '25 15:01 philipp-spiess

I've stumbled upon this today so thanks for the workaround @blittle @philipp-spiess. I've spent an hour pulling my hair out.

@philipp-spiess Does it make sense to be somehow able to configure tailwindcss to build the CSS with simple variables instead of @property for cases like this? Or would that break other stuff?

kilobyte2007 avatar Feb 05 '25 22:02 kilobyte2007

Same issue, the shadow dom should follow isolation rules, we need a way not to rely on @property

molvqingtai avatar Feb 14 '25 19:02 molvqingtai

Noticed this as well. Some css-variables are not defined in my Custom Element :/

ico85 avatar Feb 19 '25 07:02 ico85

+1 Same problem(

KoJem9Ka avatar Mar 04 '25 21:03 KoJem9Ka

You can use this code to add Tailwind properties to global style sheets:

import styles from './styles.css?inline';

const shadowSheet = new CSSStyleSheet();
shadowSheet.replaceSync(styles.replace(/:root/ug, ':host'));

const globalSheet = new CSSStyleSheet();
for (const rule of shadowSheet.cssRules) {
    if (rule instanceof CSSPropertyRule) {
        globalSheet.insertRule(rule.cssText);
    }
}

document.adoptedStyleSheets.push(globalSheet);

export class MyComponent extends HTMLElement {
    constructor() {
        super();

        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.adoptedStyleSheets = [shadowSheet];

        // ...
    }
}

Or you can replace @property with variables like this:

import styles from './styles.css?inline';

const shadowSheet = new CSSStyleSheet();
shadowSheet.replaceSync(styles.replace(/:root/ug, ':host'));

const properties = [];
for (const rule of shadowSheet.cssRules) {
  if (rule instanceof CSSPropertyRule) {
    if (rule.initialValue) {
      properties.push(`${rule.name}: ${rule.initialValue}`);
    }
  }
}
shadowSheet.insertRule(`:host { ${properties.join('; ')} }`);

export class MyComponent extends HTMLElement {
    constructor() {
        super();

        const shadowRoot = this.attachShadow({ mode: 'open' });
        shadowRoot.adoptedStyleSheets = [shadowSheet];

        // ...
    }
}

meefik avatar Mar 19 '25 17:03 meefik

I stumbled upon this too. The decision to go for @property makes Tailwind 4 unusable for the Shadow DOM, which is very frustrating. If @property is really needed for something like animating drop shadows (it this so important?), then the developers should at least have included a configuration setting that makes something as simple as a drop shadow work in the Shadow DOM. In my opinion.

sschultze avatar Apr 18 '25 10:04 sschultze

This really makes me to consider about other css options rather than tailwind

mengxi-ream avatar Apr 18 '25 11:04 mengxi-ream

I am using vite and import css as a url, then manully load it to shadow root with link element. Many styles are broken. I manually copied some properties parts from the outputs and pasted in my global.css and it solved my problem.

The original output is, and I removed the @supports part

@layer properties {
    @supports (((-webkit-hyphens: none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))) {
        *,:before,:after,::backdrop {
            --tw-translate-x:0;
            --tw-translate-y: 0;
            --tw-translate-z: 0;
            --tw-rotate-x: initial;
            --tw-rotate-y: initial;
            --tw-rotate-z: initial;
            --tw-skew-x: initial;
            --tw-skew-y: initial;
            --tw-space-y-reverse: 0;
            --tw-space-x-reverse: 0;
            --tw-border-style: solid;
            --tw-gradient-position: initial;
            --tw-gradient-from: #0000;
            --tw-gradient-via: #0000;
            --tw-gradient-to: #0000;
            --tw-gradient-stops: initial;
            --tw-gradient-via-stops: initial;
            --tw-gradient-from-position: 0%;
            --tw-gradient-via-position: 50%;
            --tw-gradient-to-position: 100%;
            --tw-leading: initial;
            --tw-font-weight: initial;
            --tw-tracking: initial;
            --tw-shadow: 0 0 #0000;
            --tw-shadow-color: initial;
            --tw-shadow-alpha: 100%;
            --tw-inset-shadow: 0 0 #0000;
            --tw-inset-shadow-color: initial;
            --tw-inset-shadow-alpha: 100%;
            --tw-ring-color: initial;
            --tw-ring-shadow: 0 0 #0000;
            --tw-inset-ring-color: initial;
            --tw-inset-ring-shadow: 0 0 #0000;
            --tw-ring-inset: initial;
            --tw-ring-offset-width: 0px;
            --tw-ring-offset-color: #fff;
            --tw-ring-offset-shadow: 0 0 #0000;
            --tw-outline-style: solid;
            --tw-blur: initial;
            --tw-brightness: initial;
            --tw-contrast: initial;
            --tw-grayscale: initial;
            --tw-hue-rotate: initial;
            --tw-invert: initial;
            --tw-opacity: initial;
            --tw-saturate: initial;
            --tw-sepia: initial;
            --tw-drop-shadow: initial;
            --tw-drop-shadow-color: initial;
            --tw-drop-shadow-alpha: 100%;
            --tw-drop-shadow-size: initial;
            --tw-backdrop-blur: initial;
            --tw-backdrop-brightness: initial;
            --tw-backdrop-contrast: initial;
            --tw-backdrop-grayscale: initial;
            --tw-backdrop-hue-rotate: initial;
            --tw-backdrop-invert: initial;
            --tw-backdrop-opacity: initial;
            --tw-backdrop-saturate: initial;
            --tw-backdrop-sepia: initial;
            --tw-duration: initial;
            --tw-content: ""
        }
    }
}

This is what I copied

@layer properties {
    *,:before,:after,::backdrop {
        --tw-translate-x: 0;
        --tw-translate-y: 0;
        --tw-translate-z: 0;
        --tw-rotate-x: initial;
        --tw-rotate-y: initial;
        --tw-rotate-z: initial;
        --tw-skew-x: initial;
        --tw-skew-y: initial;
        --tw-space-y-reverse: 0;
        --tw-space-x-reverse: 0;
        --tw-border-style: solid;
        --tw-gradient-position: initial;
        --tw-gradient-from: #0000;
        --tw-gradient-via: #0000;
        --tw-gradient-to: #0000;
        --tw-gradient-stops: initial;
        --tw-gradient-via-stops: initial;
        --tw-gradient-from-position: 0%;
        --tw-gradient-via-position: 50%;
        --tw-gradient-to-position: 100%;
        --tw-leading: initial;
        --tw-font-weight: initial;
        --tw-tracking: initial;
        --tw-shadow: 0 0 #0000;
        --tw-shadow-color: initial;
        --tw-shadow-alpha: 100%;
        --tw-inset-shadow: 0 0 #0000;
        --tw-inset-shadow-color: initial;
        --tw-inset-shadow-alpha: 100%;
        --tw-ring-color: initial;
        --tw-ring-shadow: 0 0 #0000;
        --tw-inset-ring-color: initial;
        --tw-inset-ring-shadow: 0 0 #0000;
        --tw-ring-inset: initial;
        --tw-ring-offset-width: 0px;
        --tw-ring-offset-color: #fff;
        --tw-ring-offset-shadow: 0 0 #0000;
        --tw-outline-style: solid;
        --tw-blur: initial;
        --tw-brightness: initial;
        --tw-contrast: initial;
        --tw-grayscale: initial;
        --tw-hue-rotate: initial;
        --tw-invert: initial;
        --tw-opacity: initial;
        --tw-saturate: initial;
        --tw-sepia: initial;
        --tw-drop-shadow: initial;
        --tw-drop-shadow-color: initial;
        --tw-drop-shadow-alpha: 100%;
        --tw-drop-shadow-size: initial;
        --tw-backdrop-blur: initial;
        --tw-backdrop-brightness: initial;
        --tw-backdrop-contrast: initial;
        --tw-backdrop-grayscale: initial;
        --tw-backdrop-hue-rotate: initial;
        --tw-backdrop-invert: initial;
        --tw-backdrop-opacity: initial;
        --tw-backdrop-saturate: initial;
        --tw-backdrop-sepia: initial;
        --tw-duration: initial;
        --tw-content: ""
    }
}

intellild avatar Apr 29 '25 14:04 intellild

Dealing with the same issue. I also found out that it is enough to remove (-webkit-hyphens: none) from the condition.

i.e. essentially do `.replace('((-webkit-hyphens: none)) and', '')

thes01 avatar May 27 '25 15:05 thes01

I am using vite and import css as a url, then manully load it to shadow root with link element. Many styles are broken. I manually copied some properties parts from the outputs and pasted in my global.css and it solved my problem.

The original output is, and I removed the @supports part

@layer properties {
    @supports (((-webkit-hyphens: none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))) {
        *,:before,:after,::backdrop {
            --tw-translate-x:0;
            --tw-translate-y: 0;
            --tw-translate-z: 0;
            --tw-rotate-x: initial;
            --tw-rotate-y: initial;
            --tw-rotate-z: initial;
            --tw-skew-x: initial;
            --tw-skew-y: initial;
            --tw-space-y-reverse: 0;
            --tw-space-x-reverse: 0;
            --tw-border-style: solid;
            --tw-gradient-position: initial;
            --tw-gradient-from: #0000;
            --tw-gradient-via: #0000;
            --tw-gradient-to: #0000;
            --tw-gradient-stops: initial;
            --tw-gradient-via-stops: initial;
            --tw-gradient-from-position: 0%;
            --tw-gradient-via-position: 50%;
            --tw-gradient-to-position: 100%;
            --tw-leading: initial;
            --tw-font-weight: initial;
            --tw-tracking: initial;
            --tw-shadow: 0 0 #0000;
            --tw-shadow-color: initial;
            --tw-shadow-alpha: 100%;
            --tw-inset-shadow: 0 0 #0000;
            --tw-inset-shadow-color: initial;
            --tw-inset-shadow-alpha: 100%;
            --tw-ring-color: initial;
            --tw-ring-shadow: 0 0 #0000;
            --tw-inset-ring-color: initial;
            --tw-inset-ring-shadow: 0 0 #0000;
            --tw-ring-inset: initial;
            --tw-ring-offset-width: 0px;
            --tw-ring-offset-color: #fff;
            --tw-ring-offset-shadow: 0 0 #0000;
            --tw-outline-style: solid;
            --tw-blur: initial;
            --tw-brightness: initial;
            --tw-contrast: initial;
            --tw-grayscale: initial;
            --tw-hue-rotate: initial;
            --tw-invert: initial;
            --tw-opacity: initial;
            --tw-saturate: initial;
            --tw-sepia: initial;
            --tw-drop-shadow: initial;
            --tw-drop-shadow-color: initial;
            --tw-drop-shadow-alpha: 100%;
            --tw-drop-shadow-size: initial;
            --tw-backdrop-blur: initial;
            --tw-backdrop-brightness: initial;
            --tw-backdrop-contrast: initial;
            --tw-backdrop-grayscale: initial;
            --tw-backdrop-hue-rotate: initial;
            --tw-backdrop-invert: initial;
            --tw-backdrop-opacity: initial;
            --tw-backdrop-saturate: initial;
            --tw-backdrop-sepia: initial;
            --tw-duration: initial;
            --tw-content: ""
        }
    }
}

This is what I copied

@layer properties {
    *,:before,:after,::backdrop {
        --tw-translate-x: 0;
        --tw-translate-y: 0;
        --tw-translate-z: 0;
        --tw-rotate-x: initial;
        --tw-rotate-y: initial;
        --tw-rotate-z: initial;
        --tw-skew-x: initial;
        --tw-skew-y: initial;
        --tw-space-y-reverse: 0;
        --tw-space-x-reverse: 0;
        --tw-border-style: solid;
        --tw-gradient-position: initial;
        --tw-gradient-from: #0000;
        --tw-gradient-via: #0000;
        --tw-gradient-to: #0000;
        --tw-gradient-stops: initial;
        --tw-gradient-via-stops: initial;
        --tw-gradient-from-position: 0%;
        --tw-gradient-via-position: 50%;
        --tw-gradient-to-position: 100%;
        --tw-leading: initial;
        --tw-font-weight: initial;
        --tw-tracking: initial;
        --tw-shadow: 0 0 #0000;
        --tw-shadow-color: initial;
        --tw-shadow-alpha: 100%;
        --tw-inset-shadow: 0 0 #0000;
        --tw-inset-shadow-color: initial;
        --tw-inset-shadow-alpha: 100%;
        --tw-ring-color: initial;
        --tw-ring-shadow: 0 0 #0000;
        --tw-inset-ring-color: initial;
        --tw-inset-ring-shadow: 0 0 #0000;
        --tw-ring-inset: initial;
        --tw-ring-offset-width: 0px;
        --tw-ring-offset-color: #fff;
        --tw-ring-offset-shadow: 0 0 #0000;
        --tw-outline-style: solid;
        --tw-blur: initial;
        --tw-brightness: initial;
        --tw-contrast: initial;
        --tw-grayscale: initial;
        --tw-hue-rotate: initial;
        --tw-invert: initial;
        --tw-opacity: initial;
        --tw-saturate: initial;
        --tw-sepia: initial;
        --tw-drop-shadow: initial;
        --tw-drop-shadow-color: initial;
        --tw-drop-shadow-alpha: 100%;
        --tw-drop-shadow-size: initial;
        --tw-backdrop-blur: initial;
        --tw-backdrop-brightness: initial;
        --tw-backdrop-contrast: initial;
        --tw-backdrop-grayscale: initial;
        --tw-backdrop-hue-rotate: initial;
        --tw-backdrop-invert: initial;
        --tw-backdrop-opacity: initial;
        --tw-backdrop-saturate: initial;
        --tw-backdrop-sepia: initial;
        --tw-duration: initial;
        --tw-content: ""
    }
}

Notice this solution too, but tampering is just too hacky and can't easily integrate in my current build pipeline, wonder if tailwind can have a config to opt out the @property rule, this feature are clearly not ready for massive use i suppose... see also here

HonmaMeikodesu avatar Jun 23 '25 07:06 HonmaMeikodesu

Dealing with the same issue. I also found out that it is enough to remove (-webkit-hyphens: none) from the condition.

i.e. essentially do `.replace('((-webkit-hyphens: none)) and', '')

This code is available: .replace("(-webkit-hyphens: none) and", "")

yuan1238y avatar Jun 25 '25 01:06 yuan1238y

Can this be fixed? 🙏

Even border utility doesn't work, because --tw-border-style is not set. I can confirm that removing (-webkit-hyphens: none) solves the issue.

I have a big legacy app in bootstrap, and I'm trying to migrate it to tailwind. It's no easy thing, i need to do it step by step. I can't do it page by page, because components reuse oneanother.

I can't just add tailwind, because the styles gets overriden - for example lg:hidden doesn't work, if something in bootstrap sets display. The only way for me to try to migrate tailwind is to scope it with shadowDom progresively. Adding prefix doesn't work either, because so what that I add tw prefix, if bootstrap still adds display and utilities don't work.

Because tailwind uses layers, it's automatically lower precedence than bootstrap.

I can't use important either, because bootstrap utilities already use !important on everything.

Shadow root is the only way to migrate reliably.

danon avatar Jul 14 '25 10:07 danon

... I can't just add tailwind, because the styles gets overriden - for example lg:hidden doesn't work, if something in bootstrap sets display. The only way for me to try to migrate tailwind is to scope it with shadowDom progresively. Adding prefix doesn't work either, because so what that I add tw prefix, if bootstrap still adds display and utilities don't work.

Because tailwind uses layers, it's automatically lower precedence than bootstrap.

I can't user important, because I already have a lot of tailwind components, and I would have to add ! to to all of the classes (because I don't know which would be overriden by bootstrap).

Shadow root is the only way to migrate reliably.

Hey @danon, this is not completely related to the original issue. But I ended up here having similar migration problem as yours, with library similar to bootstrap. The solution for unlayered styles (bootstrap) overriding layered ones (tailwind), that was satisfactory to me, was importing that library's styles in a new cascade layer with lower priority then tailwind's layers. So tailwind classes always override rules from a lower layer. I hope this might help.

Also, in my case, I gave up on using shadowroot because it breaks the expected behavior with radix modal focus-trap, etc.

bcacan avatar Jul 24 '25 12:07 bcacan

Also, in my case, I gave up on using shadowroot because it breaks the expected behavior with radix modal focus-trap, etc.

Ah, I was creating an embeddable piece and was leveraging shadow dom to block some of the cascading styles from the host website. I knew I read about shadow dom breaking accessibility and weird things happening within, thanks for stating what broke for you. I'm using Shadcn which depends on radix-ui.

Hmm, I guess the next best thing is to create a reset with all: revert before importing in TW and app styles.

kerryj89 avatar Jul 25 '25 17:07 kerryj89

Hmm, I guess the next best thing is to create a reset with all: revert before importing in TW and app styles.

Good thinking, but won't work with !important and styles that have higher specificity. :/

danon avatar Jul 25 '25 17:07 danon

I can't believe tailwind v4 doesn't fully support shadow dom, how can you launch v4 if a major part (shadow dom) are not fully supported, well i guess we going back to v3

Hamziss avatar Jul 28 '25 08:07 Hamziss

I can't believe tailwind v4 doesn't fully support shadow dom, how can you launch v4 if a major part (shadow dom) are not fully supported, well i guess we going back to v3 until this crap is resolved

Yes, this is annoying, but please do moderate your conduct.

hmans avatar Jul 28 '25 12:07 hmans

Dealing with the same issue. I also found out that it is enough to remove (-webkit-hyphens: none) from the condition. i.e. essentially do `.replace('((-webkit-hyphens: none)) and', '')

This code is available: .replace("(-webkit-hyphens: none) and", "")

Just noticed this was missing a set of parenthesis which caught me out!

The version mentioned in the long comment above worked for me: .replace("((-webkit-hyphens:none)) and", "");

james-whittington1 avatar Aug 06 '25 06:08 james-whittington1

Just noticed this was missing a set of parenthesis which caught me out!

There seems to be a difference between development mode and when building the project with vite.
To have it work in both development mode and when building, both cases need to be covered:

.replace("((-webkit-hyphens:none)) and ", "").replace("(-webkit-hyphens: none) and ", "")

This whole issue is most frustrating indeed.

patchee500 avatar Aug 25 '25 23:08 patchee500

This is probably closest to a fully working system (atleast until I find myself pressing f5 10 times when I see my styles not getting applied only to realize tailwind isn't getting along with shadow dom :) )

Would keep updating this hopefully if I find more problems and their potential solutions

import React from 'react';
import ReactDOM from 'react-dom/client';
import ReactShadowRoot from 'react-shadow-root';
import tailwindStyles from './index.css?inline'; // Tailwind compiled CSS

// --- Tailwind normalizer for ShadowRoot ---
function normalizeTailwind(css: string): string {
  return css
    .replace(/:root\b/g, ':host')
    .replaceAll('((-webkit-hyphens:none)) and ', '')
    .replaceAll('(-webkit-hyphens: none) and ', '');
}

// --- Demo component ---
function Demo() {
  return (
    <div className="p-4 bg-blue-500 text-white rounded">
      ShadowRoot + Tailwind 🚀
    </div>
  );
}

// --- Setup widget container ---
const container = document.createElement('div');
document.body.appendChild(container);

// --- Render inside shadow root ---
const root = ReactDOM.createRoot(container);
root.render(
  <ReactShadowRoot>
    <style>{normalizeTailwind(tailwindStyles)}</style>
    <Demo />
  </ReactShadowRoot>
);

ahmed-z0 avatar Aug 26 '25 04:08 ahmed-z0

My solution for Vue web component app:

vite.config.ts

function extractProperties(code: string) {
  const propertyRegex = /@property\s+([-\w]+)\s*\{([^}]+)}/g
  const properties = []
  let match

  while ((match = propertyRegex.exec(code)) !== null) {
    const name = match[1]
    const body = match[2]

    const syntaxMatch = body.match(/syntax:\s*([^;]+);?/i)
    const inheritsMatch = body.match(/inherits:\s*([^;]+);?/i)
    const initialValueMatch = body.match(/initial-value:\s*([^;]+);?/i)

    properties.push({
      name,
      syntax: syntaxMatch ? syntaxMatch[1].trim().replace(/"/g, '') : '*',
      inherits: inheritsMatch ? inheritsMatch[1].trim() === 'true' : false,
      initialValue: initialValueMatch ? initialValueMatch[1].trim().replace(/"/g, '') : undefined,
    })
  }

  const cleanedCode = code.replace(propertyRegex, '').trim()

  return { properties, cleanedCode }
}

export default {
// ...
plugins: [
  tailwindcss(),
  {
      name: 'tailwindcss-properties',
      transform(code: string, id: string) {
        if (id.endsWith('.css') && id.includes('app.vue')) {
          const { properties, cleanedCode } = extractProperties(code)

          writeFileSync('./.vite/tailwindcss-properties.json', JSON.stringify(properties, null, 2))

          return cleanedCode
        }
      },
    }
]
}

app.vue

<template>
Vue Code
</template>
<style>
  @import "./app.css";
</style>

app.ts

import CSSProperties from '../.vite/tailwindcss-properties.json' assert { type: 'json' }

CSSProperties.forEach(registerProperty)

lubomirblazekcz avatar Sep 02 '25 09:09 lubomirblazekcz

This is what worked for me using vite - I hope it's not too fragile: (it's copied from another issue, I added the vite wrapper around postcss)

/**
 * PostCSS plugin to convert @property declarations to CSS custom properties
 * This is needed because tailwind uses @property which is not supported by shadowdom
 */
const property_to_custom_prop = () => ({
	postcssPlugin: 'postcss-property-to-custom-prop',
	prepare() {
		const properties = [];

		return {
			AtRule: {
				property: (rule) => {
					const property_name = rule.params.match(/--[\w-]+/)?.[0];
					let initial_value = '';

					rule.walkDecls('initial-value', (decl) => {
						initial_value = decl.value;
					});

					if (property_name && initial_value) {
						properties.push({ name: property_name, value: initial_value });
						rule.remove();
					}
				},
			},
			OnceExit(root, { Rule, Declaration }) {
				if (properties.length > 0) {
					const root_rule = new Rule({ selector: ':root, :host' });

					for (const prop of properties) {
						root_rule.append(
							new Declaration({
								prop: prop.name,
								value: prop.value,
							}),
						);
					}

					root.prepend(root_rule);
				}
			},
		};
	},
})
property_to_custom_prop.postcss = true;

export default defineConfig({
	root: __dirname,
	plugins: [
		vue(),
		tailwindcss(),
		{
			name: 'vite-plugin-property-to-custom-prop',
			config() {
				return {
					css: {
						postcss: {
							plugins: [ property_to_custom_prop() ],
						}
					}
				}
			}
		}
	],

cosbgn avatar Sep 03 '25 13:09 cosbgn

think a great solution would be if tailwind would add somewthing like import @shadowdom which would import all @property classes, so for shadowdom projects we could simply do:

@import "tailwindcss"
@import "shadowdom"

"shadowdom" would add:

:host {
    --tw-divide-y-reverse: 0;
    --tw-border-style: solid;
    --tw-font-weight: initial;
    --tw-tracking: initial;
    --tw-translate-x: 0;
    --tw-translate-y: 0;
    --tw-translate-z: 0;
    --tw-rotate-x: rotateX(0);
    --tw-rotate-y: rotateY(0);
    --tw-rotate-z: rotateZ(0);
    --tw-skew-x: skewX(0);
    --tw-skew-y: skewY(0);
    --tw-space-x-reverse: 0;
    --tw-gradient-position: initial;
    --tw-gradient-from: #0000;
    --tw-gradient-via: #0000;
    --tw-gradient-to: #0000;
    --tw-gradient-stops: initial;
    --tw-gradient-via-stops: initial;
    --tw-gradient-from-position: 0%;
    --tw-gradient-via-position: 50%;
    --tw-gradient-to-position: 100%;
    --tw-shadow: 0 0 #0000;
    --tw-shadow-color: initial;
    --tw-inset-shadow: 0 0 #0000;
    --tw-inset-shadow-color: initial;
    --tw-ring-color: initial;
    --tw-ring-shadow: 0 0 #0000;
    --tw-inset-ring-color: initial;
    --tw-inset-ring-shadow: 0 0 #0000;
    --tw-ring-inset: initial;
    --tw-ring-offset-width: 0px;
    --tw-ring-offset-color: #fff;
    --tw-ring-offset-shadow: 0 0 #0000;
    --tw-blur: initial;
    --tw-brightness: initial;
    --tw-contrast: initial;
    --tw-grayscale: initial;
    --tw-hue-rotate: initial;
    --tw-invert: initial;
    --tw-opacity: initial;
    --tw-saturate: initial;
    --tw-sepia: initial;
    --tw-drop-shadow: initial;
    --tw-duration: initial;
    --tw-ease: initial;
}

The issue with adding them manually is that I'm responsible of keeping the list updated and I never know if something will work or not, so ideally I would love to have this responsibility shifted to tailwind, so that if they make internal updates, also "shadowdom" get's updated

cosbgn avatar Sep 05 '25 08:09 cosbgn

... I can't just add tailwind, because the styles gets overriden - for example lg:hidden doesn't work, if something in bootstrap sets display. The only way for me to try to migrate tailwind is to scope it with shadowDom progresively. Adding prefix doesn't work either, because so what that I add tw prefix, if bootstrap still adds display and utilities don't work. Because tailwind uses layers, it's automatically lower precedence than bootstrap. I can't user important, because I already have a lot of tailwind components, and I would have to add ! to to all of the classes (because I don't know which would be overriden by bootstrap). Shadow root is the only way to migrate reliably.

Hey @danon, this is not completely related to the original issue. But I ended up here having similar migration problem as yours, with library similar to bootstrap. The solution for unlayered styles (bootstrap) overriding layered ones (tailwind), that was satisfactory to me, was importing that library's styles in a new cascade layer with lower priority then tailwind's layers. So tailwind classes always override rules from a lower layer. I hope this might help.

But bootstrap uses !important all of the place, wouldn't that override tailwind, even in a lower cascade layer?

danon avatar Sep 05 '25 11:09 danon

For reference https://developer.chrome.com/docs/css-ui/css-names discusses how @property is supposed to work with Shadow DOM and how it actually behaves. https://github.com/w3c/csswg-drafts/issues/10541 is a specification issue concerning @property and Shadow DOM.

robertknight avatar Sep 12 '25 13:09 robertknight

Want to emphasize this answer, since this is no problem of @property itself, but of the enclosing @support block. Especially the -webkit-hyphens: none condition.

This is probably closest to a fully working system (atleast until I find myself pressing f5 10 times when I see my styles not getting applied only to realize tailwind isn't getting along with shadow dom :) )

Would keep updating this hopefully if I find more problems and their potential solutions

import React from 'react'; import ReactDOM from 'react-dom/client'; import ReactShadowRoot from 'react-shadow-root'; import tailwindStyles from './index.css?inline'; // Tailwind compiled CSS

// --- Tailwind normalizer for ShadowRoot --- function normalizeTailwind(css: string): string { return css .replace(/:root\b/g, ':host') .replaceAll('((-webkit-hyphens:none)) and ', '') .replaceAll('(-webkit-hyphens: none) and ', ''); }

// --- Demo component --- function Demo() { return ( <div className="p-4 bg-blue-500 text-white rounded"> ShadowRoot + Tailwind 🚀 ); }

// --- Setup widget container --- const container = document.createElement('div'); document.body.appendChild(container);

// --- Render inside shadow root --- const root = ReactDOM.createRoot(container); root.render( <ReactShadowRoot> <Demo /> </ReactShadowRoot> );

Alletkla avatar Jan 08 '26 09:01 Alletkla