diff --git a/CHANGELOG.md b/CHANGELOG.md index aa9356494..a8a0cbc1a 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 - Allow `@tailwindcss/cli` in `--watch` mode to use polling with `--poll` when filesystem events are unreliable or unavailable ([#20297](https://github.com/tailwindlabs/tailwindcss/pull/20297)) +- Canonicalization: match arbitrary hex colors against theme colors case-insensitively (e.g. `bg-[#fff]` and `bg-[#FFF]` → `bg-white`) ([#20298](https://github.com/tailwindlabs/tailwindcss/pull/20298)) ## [4.3.2] - 2026-06-26 diff --git a/packages/tailwindcss/src/canonicalize-candidates.test.ts b/packages/tailwindcss/src/canonicalize-candidates.test.ts index c01ee78e1..03229f450 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.test.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.test.ts @@ -316,6 +316,10 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', ['[color:var(--color-red-500)]', 'text-red-500'], ['[background-color:var(--color-red-500)]', 'bg-red-500'], + // Arbitrary property to named utility (case insensitive) + ['[color:#fff]', 'text-white'], + ['[color:#FFF]', 'text-white'], + // Arbitrary property with modifier to named functional utility with modifier ['[color:var(--color-red-500)]/25', 'text-red-500/25'], @@ -354,6 +358,7 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', --*: initial; --spacing: 0.25rem; --color-red-500: red; + --color-white: #fff; /* Equivalent of blue-500/50 */ --color-primary: color-mix(in oklab, oklch(62.3% 0.214 259.815) 50%, transparent); @@ -394,6 +399,10 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', ['bg-[theme(colors.red.500/75%)]', 'bg-red-500/75'], ['bg-[theme(colors.red.500/12.34%)]', 'bg-red-500/[12.34%]'], + // Arbitrary value with different casing + ['bg-[#fff]', 'bg-white'], + ['bg-[#FFF]', 'bg-white'], + // Values that don't contain only `theme(…)` calls should not be converted to // use a modifier since the color is not the whole value. [ @@ -478,6 +487,9 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', ])(testName, { timeout }, async (candidate, expected) => { let input = css` @import 'tailwindcss'; + @theme { + --color-white: #fff; + } ` await expectCanonicalization(input, candidate, expected) @@ -1508,4 +1520,30 @@ describe('regressions', () => { expect(designSystem.canonicalizeCandidates(['px-[calc(1rem+0px)]'], options)).toEqual(['px-4']) }) + + // https://github.com/tailwindlabs/tailwindcss/issues/20295 + test('canonicalizations work when casing of arbitrary values is different', async () => { + let designSystem = await designSystems.get(__dirname).get(css` + @import 'tailwindcss'; + @theme { + --color-brand-purple: #3f3cbb; + } + `) + + let options: CanonicalizeOptions = { + collapse: true, + logicalToPhysical: true, + rem: 16, + } + + expect(designSystem.canonicalizeCandidates(['bg-[#3F3cbb]'], options)).toEqual([ + 'bg-brand-purple', + ]) + expect(designSystem.canonicalizeCandidates(['bg-[#3F3Cbb]'], options)).toEqual([ + 'bg-brand-purple', + ]) + expect(designSystem.canonicalizeCandidates(['bg-[#3F3CBB]'], options)).toEqual([ + 'bg-brand-purple', + ]) + }) }) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 6faf346ed..eb9d8697c 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -2425,6 +2425,12 @@ function createUtilitySignatureCache( }) } +// #RGB +// #RGBA +// #RRGGBB +// #RRGGBBAA +const HEX_REGEX = /#(?:[a-f0-9]{8}|[a-f0-9]{6}|[a-f0-9]{4}|[a-f0-9]{3})/gi + // Optimize the CSS AST to make it suitable for signature comparison. We want to // expand declarations, ignore comments, sort declarations etc... function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: SignatureOptions) { @@ -2536,6 +2542,18 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si return a.property.localeCompare(b.property) }) } + + // + else if (node.kind === 'declaration' && node.value) { + // Leave CSS variables alone + if (node.property[0] === '-' && node.property[1] === '-') return + + // Ensure hex colors are always lowercased + { + HEX_REGEX.lastIndex = 0 + node.value = node.value.replace(HEX_REGEX, (color) => color.toLowerCase()) + } + } }, })