Skip to content

@property isn't supported in shadow roots #15005

Open

Description

@blittle

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: w3c/css-houdini-drafts#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;
          }
        },
      },
    ],
  };
});

Activity

  1. ErwinAI commented on Nov 22, 2024

    @ErwinAI

    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.

  2. rose21wiley commented on Nov 28, 2024

    @rose21wiley
  3. philipp-spiess commented on Jan 29, 2025

    @philipp-spiess
    Contributor

    @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)
  4. kilobyte2007 commented on Feb 5, 2025

    @kilobyte2007

    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?

  5. molvqingtai commented on Feb 14, 2025

    @molvqingtai

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

  6. KoJem9Ka commented on Mar 4, 2025

    @KoJem9Ka

    +1 Same problem(

  7. meefik commented on Mar 19, 2025

    @meefik

    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];
    
            // ...
        }
    }
  8. 24 remaining items

  9. robertknight commented on Sep 12, 2025

    @robertknight

    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. w3c/csswg-drafts#10541 is a specification issue concerning @property and Shadow DOM.

  10. added 2 commits that reference this issue on Oct 9, 2025
  11. added a commit that references this issue on Oct 14, 2025
  12. added a commit that references this issue on Dec 16, 2025
  13. Alletkla commented on Jan 8, 2026

    @Alletkla

    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 (


    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(

    <style>{normalizeTailwind(tailwindStyles)}</style>


    );

  14. danimalweb commented on Jan 20, 2026

    @danimalweb

    I just ran into this while using Shadow DOM, and I was confused about why some variables didn't work. I would love to see this fixed or a config workaround.

  15. Alletkla commented on Jan 21, 2026

    @Alletkla

    @danimalweb just published a small vite plugin that does the job for me: https://www.npmjs.com/package/vite-plugin-tailwind-shadowdom

    You might give it a try :)

    Edit: updated for safari

  16. rxliuli commented on Feb 12, 2026

    @rxliuli

    Sharing a runtime solution

    export function addStyle(shadow: ShadowRoot, styles: string[]) {
      const css = styles.join('\n').replaceAll(':root', ':host')
    
      // Extract @property declarations and hoist to document head,
      // because @property doesn't work inside Shadow DOM <style> elements.
      // https://github.com/tailwindlabs/tailwindcss/issues/15005
      const propertyRules: string[] = []
      const shadowCss = css.replace(/@property\s+[^{]+\{[^}]*\}/g, (match) => {
        propertyRules.push(match)
        return ''
      })
    
      if (propertyRules.length > 0) {
        const propStyle = document.createElement('style')
        propStyle.textContent = propertyRules.join('\n')
        document.head.appendChild(propStyle)
      }
    
      const style = document.createElement('style')
      style.textContent = shadowCss
      if (shadow.firstChild) {
        shadow.insertBefore(style, shadow.firstChild)
      } else {
        shadow.appendChild(style)
      }
    }
  17. khalidsaidi commented on Mar 25, 2026

    @khalidsaidi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions