Skip to content

Make polyfill work when the theme variable resolves to another var #17562

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
49 changes: 40 additions & 9 deletions packages/tailwindcss/src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand All @@ -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<string>()
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(','),
Expand Down
58 changes: 58 additions & 0 deletions packages/tailwindcss/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down