Skip to content
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 @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Skip ignored directories entirely when computing watch globs (`scanner.globs`), instead of walking their full contents on every rebuild ([#20408](https://github.com/tailwindlabs/tailwindcss/pull/20408))
- Oxide: drop invalid UTF-8 candidates ([#20389](https://github.com/tailwindlabs/tailwindcss/pull/20389))
- `@tailwindcss/vite` no longer forces a full page reload for external files (e.g.: `.php` files) ([#20414](https://github.com/tailwindlabs/tailwindcss/issues/20414))
- Canonicalization: don't merge utilities that reference different theme variables set to CSS-wide keywords like `unset` ([#20417](https://github.com/tailwindlabs/tailwindcss/pull/20417))

## [4.3.3] - 2026-07-16

Expand Down
22 changes: 22 additions & 0 deletions packages/tailwindcss/src/canonicalize-candidates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1638,4 +1638,26 @@ describe('regressions', () => {
'lg:flex',
])
})

// https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1610
test('does not merge utilities whose theme variables resolve to CSS-wide keywords', async () => {
let designSystem = await __unstable__loadDesignSystem(
css`
@tailwind utilities;
@theme {
--foreground: unset;
--default: unset;
}
@theme inline {
--color-foreground: var(--foreground);
--color-default-soft-hover: color-mix(in oklab, var(--default) 60%, transparent);
}
`,
{ base: __dirname },
)

expect(
designSystem.canonicalizeCandidates(['text-foreground/60', 'text-default-soft-hover']),
).toEqual(['text-foreground/60', 'text-default-soft-hover'])
})
})
38 changes: 30 additions & 8 deletions packages/tailwindcss/src/canonicalize-candidates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2563,6 +2563,25 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si
return ast
}

// Variables whose theme value is a CSS-wide keyword (e.g.: `unset`) are never
// inlined. These are typically registered as a placeholder to be re-assigned at
// runtime, so two variables that share such a value are not interchangeable.
//
// E.g.:
//
// ```css
// @theme {
// --foreground: unset;
// --background: unset;
// }
// ```
//
// Inlining would make `text-(--foreground)` and `text-(--background)` produce
// the same signature `color: unset`, even though they are different at runtime.
//
// https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/Data_types#css-wide_keywords
const CSS_WIDE_KEYWORDS = ['initial', 'inherit', 'revert', 'revert-layer', 'revert-rule', 'unset']

// Resolve theme values to their inlined value.
//
// E.g.:
Expand All @@ -2578,8 +2597,8 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si
// }
// ```
//
// Which conveniently will be equivalent to: `text-red-500` when we inline
// the value.
// Which conveniently will be equivalent to: `text-red-500` when we inline the
// value.
//
// Without inlining:
// ```css
Expand All @@ -2595,13 +2614,13 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si
// }
// ```
//
// Recently we made sure that utilities like `text-red-500` also generate
// the fallback value for usage in `@reference` mode.
// Recently we made sure that utilities like `text-red-500` also generate the
// fallback value for usage in `@reference` mode.
//
// The second assumption is that if you use `var(--key, fallback)` that
// happens to match a known variable _and_ its inlined value. Then we can
// replace it with the inlined variable. This allows us to handle custom
// `@theme` and `@theme inline` definitions.
// The second assumption is that if you use `var(--key, fallback)` that happens
// to match a known variable _and_ its inlined value. Then we can replace it
// with the inlined variable. This allows us to handle custom `@theme` and
// `@theme inline` definitions.
function resolveVariablesInValue(value: string, designSystem: DesignSystem): string {
let changed = false
let valueAst = ValueParser.parse(value)
Expand Down Expand Up @@ -2630,6 +2649,9 @@ function resolveVariablesInValue(value: string, designSystem: DesignSystem): str
seen.add(variable)
if (variableValue === undefined) return // Couldn't resolve the variable

// CSS-wide keywords are never inlined
if (CSS_WIDE_KEYWORDS.includes(variableValue.toLowerCase())) return

Comment thread
RobinMalfait marked this conversation as resolved.
// Inject variable fallbacks when no fallback is present yet.
//
// A fallback could consist of multiple values.
Expand Down