[bug] box-shadow doesn't work in shadow-dom due to reliance on @property #16772
Replies: 8 comments 4 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
@property
provides default values. However in shadow-dom@property
does 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
@property
since it doesn't work in shadow-dom land.Beta Was this translation helpful? Give feedback.
All reactions