[bug] box-shadow doesn't work in shadow-dom due to reliance on @property #16772
Replies: 8 comments 5 replies
-
| 
         Hey @snaptopixel! Unfortunately  The issue is that we rely on   | 
  
Beta Was this translation helpful? Give feedback.
-
| 
         Hi, Just started a new project and found this discussion. The plugin looks for  I guess i'll see some errors when it comes to animations since the css rules will not have an initial value to animate from. Anyway, here is the Plugin code: /**
 * PostCSS plugin to convert @property declarations to CSS custom properties
 * This helps with using property values inside Shadow DOM
 */
export default (opts = {}) => {
  return {
    postcssPlugin: 'postcss-property-to-custom-prop',
    prepare(result) {
      // Store all the properties we find
      const properties = [];
      return {
        AtRule: {
          property: (rule) => {
            // Extract the property name and initial value
            const propertyName = rule.params.match(/--[\w-]+/)?.[0];
            let initialValue = '';
            rule.walkDecls('initial-value', (decl) => {
              initialValue = decl.value;
            });
            if (propertyName && initialValue) {
              // Store the property
              properties.push({ name: propertyName, value: initialValue });
              // Remove the original @property rule
              rule.remove();
            }
          },
        },
        OnceExit(root, { Rule, Declaration }) {
          // If we found properties, add them to :root, :host
          if (properties.length > 0) {
            // Create the :root, :host rule using the Rule constructor from helpers
            const rootRule = new Rule({ selector: ':root, :host' });
            // Add all properties as declarations
            properties.forEach((prop) => {
              // Create a new declaration for each property
              const decl = new Declaration({
                prop: prop.name,
                value: prop.value,
              });
              rootRule.append(decl);
            });
            // Add the rule to the beginning of the CSS
            root.prepend(rootRule);
          }
        },
      };
    },
  };
};
export const postcss = true;Usage: import propertyToCustomProp from './plugins/postcss-property-to-custom-prop';
import tailwindcss from '@tailwindcss/postcss';
export default {
  plugins: [
    tailwindcss(),
    propertyToCustomProp()
  ],
};Inside my web component i'm using the  import globalCss from '@/styles/global.css?inline';
const sheet = new CSSStyleSheet();
sheet.replaceSync(globalCss);
export class MyComponent extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot!.adoptedStyleSheets = [sheet];
        // ...
    }
} | 
  
Beta Was this translation helpful? Give feedback.
-
| 
         I stumbled upon this too. The decision to go for   | 
  
Beta Was this translation helpful? Give feedback.
-
| 
         Is there an update to this? Same for :root / :host  | 
  
Beta Was this translation helpful? Give feedback.
-
| 
         Working on an app thats going to be used as a widget on other 3rd party websites hence I need to use the shadow dom and then realised some variables are not defined. Spent some time debugging and then found this, can there be an opt in. I guess I will have to handroll my css  | 
  
Beta Was this translation helpful? Give feedback.
-
| 
         @benkissi as you can read, I had the same issue, i fixed it by converting the   | 
  
Beta Was this translation helpful? Give feedback.
-
| 
         i just faced the problem, don't want to add postcss.  | 
  
Beta Was this translation helpful? Give feedback.
-
| 
         OK - I had same problem, but I needed this as a module. Also didn't want top copy properties over as the project is evolving and I want this to manage itself... What I found was the colour calculations were not working in shadow-dom and they needed to be simplified so I added a utility to convert these to RGBA and now my shadows are working nicely, this builds on @rikgirbes code and makes it adapt any shadows your using into css that shadow dom should be happy with. /**
 * PostCSS plugin to convert @property declarations to CSS custom properties
 * AND fix shadow utilities for Shadow DOM compatibility
 */
const property_to_custom_prop = () => ({
	postcssPlugin: 'postcss-property-to-custom-prop',
	prepare() {
		const properties = [];
		let shadowsProcessed = 0;
		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();
					}
				},
			},
			Rule(rule) {
				// Fix shadow utilities for Shadow DOM compatibility
				if (rule.selector.includes('shadow-')) {
					rule.walkDecls((decl) => {
						// Convert complex Tailwind shadow variables to direct values
						if (decl.prop === 'box-shadow' && (decl.value.includes('var(--tw-') || decl.value.includes('oklab('))) {
							// Handle direct oklab values in box-shadow (when not using variables)
							if (decl.value.includes('oklab(') && !decl.value.includes('var(--tw-')) {
								let shadowValue = decl.value;
								// Convert NEW oklab(from rgb()) patterns directly in box-shadow
								shadowValue = shadowValue.replace(/oklab\(from\s+rgb\((\d+)\s+(\d+)\s+(\d+)\s*\/\s*([\d.]+)\s+l\s+a\s+b\s*\/\s*(\d+)%\)\)/g, (match, r, g, b, baseAlpha, percentAlpha) => {
									const alpha = (parseFloat(percentAlpha) / 100);
									return `rgba(${r}, ${g}, ${b}, ${alpha})`;
								});
								shadowValue = shadowValue.replace(/oklab\(from\s+rgb\((\d+)\s+(\d+)\s+(\d+)\s*\/\s*([\d.]+)\)\s+l\s+a\s+b\s*\/\s*(\d+)%\)/g, (match, r, g, b, baseAlpha, percentAlpha) => {
									const alpha = (parseFloat(percentAlpha) / 100);
									return `rgba(${r}, ${g}, ${b}, ${alpha})`;
								});
								shadowValue = shadowValue.replace(/oklab\(from\s+rgb\((\d+)\s+(\d+)\s+(\d+)\s*\/\s*([\d.]+)\)\s+l\s+a\s+b\)/g, (match, r, g, b, alpha) => {
									return `rgba(${r}, ${g}, ${b}, ${alpha})`;
								});
								shadowValue = shadowValue.replace(/oklab\(from\s+rgb\((\d+)\s+(\d+)\s+(\d+)\)\s+l\s+a\s+b\s*\/\s*(\d+)%\)/g, (match, r, g, b, percentAlpha) => {
									const alpha = (parseFloat(percentAlpha) / 100);
									return `rgba(${r}, ${g}, ${b}, ${alpha})`;
								});
								decl.value = shadowValue;
								shadowsProcessed++;
								if (process.env.NODE_ENV !== 'production') {
									console.log(`🔧 PostCSS: Fixed direct shadow ${rule.selector} -> ${shadowValue}`);
								}
								return; // Don't process further if we handled direct values
							}
							// Extract shadow from --tw-shadow variable if present
							const shadowMatch = decl.value.match(/var\(--tw-shadow\)/);
							if (shadowMatch) {
								// Find the --tw-shadow declaration in the same rule
								rule.walkDecls('--tw-shadow', (shadowDecl) => {
									// Convert all shadow values dynamically
									let shadowValue = shadowDecl.value;
									// NEW: Convert oklab(from rgb()) patterns - Tailwind CSS 4 format
									// Handle: oklab(from rgb(0 0 0 / 0.1 l a b / 30%))
									shadowValue = shadowValue.replace(/oklab\(from\s+rgb\((\d+)\s+(\d+)\s+(\d+)\s*\/\s*([\d.]+)\s+l\s+a\s+b\s*\/\s*(\d+)%\)\)/g, (match, r, g, b, baseAlpha, percentAlpha) => {
										const alpha = (parseFloat(percentAlpha) / 100);
										return `rgba(${r}, ${g}, ${b}, ${alpha})`;
									});
									// Handle: oklab(from rgb(0 0 0 / 0.1) l a b / 30%)
									shadowValue = shadowValue.replace(/oklab\(from\s+rgb\((\d+)\s+(\d+)\s+(\d+)\s*\/\s*([\d.]+)\)\s+l\s+a\s+b\s*\/\s*(\d+)%\)/g, (match, r, g, b, baseAlpha, percentAlpha) => {
										const alpha = (parseFloat(percentAlpha) / 100);
										return `rgba(${r}, ${g}, ${b}, ${alpha})`;
									});
									// Handle: oklab(from rgb(0 0 0 / 0.1) l a b)
									shadowValue = shadowValue.replace(/oklab\(from\s+rgb\((\d+)\s+(\d+)\s+(\d+)\s*\/\s*([\d.]+)\)\s+l\s+a\s+b\)/g, (match, r, g, b, alpha) => {
										return `rgba(${r}, ${g}, ${b}, ${alpha})`;
									});
									// Handle: oklab(from rgb(0 0 0) l a b / 30%)
									shadowValue = shadowValue.replace(/oklab\(from\s+rgb\((\d+)\s+(\d+)\s+(\d+)\)\s+l\s+a\s+b\s*\/\s*(\d+)%\)/g, (match, r, g, b, percentAlpha) => {
										const alpha = (parseFloat(percentAlpha) / 100);
										return `rgba(${r}, ${g}, ${b}, ${alpha})`;
									});
									// Convert oklab patterns - extract alpha from oklab(0% 0 0/ALPHA)
									shadowValue = shadowValue.replace(/oklab\(0% 0 0\/\.(\d+)\)/g, (match, alpha) => {
										const alphaValue = parseFloat('0.' + alpha);
										return `rgba(0, 0, 0, ${alphaValue})`;
									});
									// Convert oklab patterns with decimal alpha - oklab(0% 0 0/.05)
									shadowValue = shadowValue.replace(/oklab\(0% 0 0\/(\.\d+)\)/g, (match, alpha) => {
										const alphaValue = parseFloat(alpha);
										return `rgba(0, 0, 0, ${alphaValue})`;
									});
									// Convert var(--tw-shadow-color,xxx) patterns
									shadowValue = shadowValue.replace(/var\(--tw-shadow-color,([^)]+)\)/g, '$1');
									// Convert hex colors with alpha to rgba - dynamically parse hex values
									shadowValue = shadowValue.replace(/#([0-9a-fA-F]{6})([0-9a-fA-F]{2})/g, (match, rgb, alpha) => {
										const r = parseInt(rgb.substr(0, 2), 16);
										const g = parseInt(rgb.substr(2, 2), 16);
										const b = parseInt(rgb.substr(4, 2), 16);
										const a = parseInt(alpha, 16) / 255;
										return `rgba(${r}, ${g}, ${b}, ${a.toFixed(3)})`;
									});
									// Set the box-shadow directly to the converted value
									decl.value = shadowValue;
									shadowsProcessed++;
									// Debug logging in development
									if (process.env.NODE_ENV !== 'production') {
										console.log(`🔧 PostCSS: Fixed shadow ${rule.selector} -> ${shadowValue}`);
									}
								});
								// Remove the Tailwind variable declarations as they're no longer needed
								rule.walkDecls((varDecl) => {
									if (varDecl.prop.startsWith('--tw-shadow') || varDecl.prop === '--tw-inset-shadow' || varDecl.prop === '--tw-ring-shadow') {
										varDecl.remove();
									}
								});
							}
						}
						// Also handle --tw-shadow-color declarations directly
						if (decl.prop === '--tw-shadow-color') {
							// Convert oklab and color-mix patterns in shadow colors
							let colorValue = decl.value;
							// Convert NEW oklab(from rgb()) patterns in color values
							colorValue = colorValue.replace(/oklab\(from\s+rgb\((\d+)\s+(\d+)\s+(\d+)\s*\/\s*([\d.]+)\s+l\s+a\s+b\s*\/\s*(\d+)%\)\)/g, (match, r, g, b, baseAlpha, percentAlpha) => {
								const alpha = (parseFloat(percentAlpha) / 100);
								return `rgba(${r}, ${g}, ${b}, ${alpha})`;
							});
							colorValue = colorValue.replace(/oklab\(from\s+rgb\((\d+)\s+(\d+)\s+(\d+)\s*\/\s*([\d.]+)\)\s+l\s+a\s+b\s*\/\s*(\d+)%\)/g, (match, r, g, b, baseAlpha, percentAlpha) => {
								const alpha = (parseFloat(percentAlpha) / 100);
								return `rgba(${r}, ${g}, ${b}, ${alpha})`;
							});
							colorValue = colorValue.replace(/oklab\(from\s+rgb\((\d+)\s+(\d+)\s+(\d+)\s*\/\s*([\d.]+)\)\s+l\s+a\s+b\)/g, (match, r, g, b, alpha) => {
								return `rgba(${r}, ${g}, ${b}, ${alpha})`;
							});
							colorValue = colorValue.replace(/oklab\(from\s+rgb\((\d+)\s+(\d+)\s+(\d+)\)\s+l\s+a\s+b\s*\/\s*(\d+)%\)/g, (match, r, g, b, percentAlpha) => {
								const alpha = (parseFloat(percentAlpha) / 100);
								return `rgba(${r}, ${g}, ${b}, ${alpha})`;
							});
							// Convert hex with alpha
							colorValue = colorValue.replace(/#([0-9a-fA-F]{6})([0-9a-fA-F]{2})/g, (match, rgb, alpha) => {
								const r = parseInt(rgb.substr(0, 2), 16);
								const g = parseInt(rgb.substr(2, 2), 16);
								const b = parseInt(rgb.substr(4, 2), 16);
								const a = parseInt(alpha, 16) / 255;
								return `rgba(${r}, ${g}, ${b}, ${a.toFixed(3)})`;
							});
							// Convert color-mix patterns
							colorValue = colorValue.replace(/color-mix\(in oklab,([^)]+)\)/g, (match, content) => {
								// Parse percentage and convert to rgba
								const percentMatch = content.match(/(\d+)%/);
								if (percentMatch) {
									const percent = parseInt(percentMatch[1]) / 100;
									return `rgba(0, 0, 0, ${percent})`;
								}
								return match;
							});
							decl.value = colorValue;
						}
					});
				}
			},
			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);
				}
				// Debug logging
				if (process.env.NODE_ENV !== 'production' && shadowsProcessed > 0) {
					console.log(`✅ PostCSS: Processed ${shadowsProcessed} shadow utilities for Shadow DOM`);
				}
			},
		};
	},
});
property_to_custom_prop.postcss = true;
module.exports = {
	plugins: [
		require('@tailwindcss/postcss'),
		property_to_custom_prop(),
		require('autoprefixer'),
	],
};edited to add support for watch (watch uses more exotic from rgb syntax)  | 
  
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
What version of Tailwind CSS are you using?
v4.0.6
What build tool (or framework if it abstracts the build tool) are you using?
tailwind-cli
What version of Node.js are you using?
v20.0.0
What browser are you using?
Chrome
What operating system are you using?
macOS
Reproduction URL
https://codepen.io/snaptopixel/pen/GgRZebj
Describe your issue
Tailwind's box-shadow based utils (ring, shadow, etc) use multiple shadow syntax with custom properties:
Since these vars don't have default/fallback values the whole box-shadow breaks if any one of them is undefined. In non-shadow this is a non-issue since
@propertyprovides default values. However in shadow-dom@propertydoes nothing, so ironically, in shadow-dom you will have no shadows on your dom.Essentially it seems there should be suitable fallbacks for any utils that currently rely on
@propertysince it doesn't work in shadow-dom land.Beta Was this translation helpful? Give feedback.
All reactions