diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f7f160d899f..d76c07b34352 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Ensure `color-mix(…)` polyfills do not cause used CSS variables to be removed ([#17555](https://github.com/tailwindlabs/tailwindcss/pull/17555)) +- Ensure the `color-mix(…)` polyfill creates fallbacks for theme variables that reference other theme variables ([#17562](https://github.com/tailwindlabs/tailwindcss/pull/17562)) ## [4.1.3] - 2025-04-04 diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index f15191118d68..3db0432dd3a9 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -542,6 +542,7 @@ export function optimizeAst( let requiresPolyfill = false ValueParser.walk(ast, (node, { replaceWith }) => { if (node.kind !== 'function' || node.value !== 'color-mix') return + let containsUnresolvableVars = false let containsCurrentcolor = false ValueParser.walk(node.nodes, (node, { replaceWith }) => { @@ -550,17 +551,47 @@ export function optimizeAst( requiresPolyfill = true return } - if (node.kind !== 'function' || node.value !== 'var') return - let firstChild = node.nodes[0] - if (!firstChild || firstChild.kind !== 'word') return - requiresPolyfill = true - let inlinedColor = designSystem.theme.resolveValue(null, [firstChild.value as any]) - if (!inlinedColor) { - containsUnresolvableVars = true - return - } + + let varNode: ValueParser.ValueAstNode | null = node + let inlinedColor: string | null = null + let seenVariables = new Set() + do { + if (varNode.kind !== 'function' || varNode.value !== 'var') return + let firstChild = varNode.nodes[0] + if (!firstChild || firstChild.kind !== 'word') return + + let variableName = firstChild.value + + if (seenVariables.has(variableName)) { + containsUnresolvableVars = true + return + } + + seenVariables.add(variableName) + + requiresPolyfill = true + + inlinedColor = designSystem.theme.resolveValue(null, [firstChild.value as any]) + if (!inlinedColor) { + containsUnresolvableVars = true + return + } + if (inlinedColor.toLowerCase() === 'currentcolor') { + containsCurrentcolor = true + return + } + + if (inlinedColor.startsWith('var(')) { + let subAst = ValueParser.parse(inlinedColor) + varNode = subAst[0] + } else { + varNode = null + } + } while (varNode) + replaceWith({ kind: 'word', value: inlinedColor }) }) + if (containsUnresolvableVars || containsCurrentcolor) { let separatorIndex = node.nodes.findIndex( (node) => node.kind === 'separator' && node.value.trim().includes(','), diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 707e8f02c875..411e3fe7b426 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -4764,6 +4764,36 @@ describe('`color-mix(…)` polyfill', () => { `) }) + it('creates an inlined variable version of the color-mix(…) usages when it resolves to a var(…) containing another theme variable', async () => { + await expect( + compileCss( + css` + @theme { + --color-red: var(--color-red-500); + --color-red-500: oklch(63.7% 0.237 25.331); + } + @tailwind utilities; + `, + ['text-red/50'], + ), + ).resolves.toMatchInlineSnapshot(` + ":root, :host { + --color-red: var(--color-red-500); + --color-red-500: oklch(63.7% .237 25.331); + } + + .text-red\\/50 { + color: #fb2c3680; + } + + @supports (color: color-mix(in lab, red, red)) { + .text-red\\/50 { + color: color-mix(in oklab, var(--color-red) 50%, transparent); + } + }" + `) + }) + it('works for color values in the first and second position', async () => { await expect( compileCss( @@ -4971,6 +5001,34 @@ describe('`color-mix(…)` polyfill', () => { `) }) + it('uses the first color value as the fallback when the `color-mix(…)` function contains theme variables that resolves to other variables', async () => { + await expect( + compileCss( + css` + @tailwind utilities; + @theme { + --color-red: var(--my-red); + } + `, + ['text-red/50'], + ), + ).resolves.toMatchInlineSnapshot(` + ".text-red\\/50 { + color: var(--color-red); + } + + @supports (color: color-mix(in lab, red, red)) { + .text-red\\/50 { + color: color-mix(in oklab, var(--color-red) 50%, transparent); + } + } + + :root, :host { + --color-red: var(--my-red); + }" + `) + }) + it('uses the first color value of the inner most `color-mix(…)` function as the fallback when nested `color-mix(…)` function all contain non-theme variables', async () => { await expect( compileCss(