From c10d285a42511d28ef36ff794592962efe20e20c Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 26 Nov 2025 11:01:13 +0100 Subject: [PATCH 1/5] add failing tests --- .../src/canonicalize-candidates.test.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.test.ts b/packages/tailwindcss/src/canonicalize-candidates.test.ts index 26a34b96465d..fd5826c4dce6 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.test.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.test.ts @@ -530,8 +530,24 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', // Default spacing scale ['w-[64rem]', 'w-256', '0.25rem'], + // Non-suggested numbers + ['gap-[7.25rem]', 'gap-29', '0.25rem'], + ['gap-[calc(7rem+0.25rem)]', 'gap-29', '0.25rem'], + ['gap-[116px]', 'gap-29', '0.25rem'], + + // Non-suggested numbers, with the same spacing scale with different + // units + ['gap-[7.25rem]', 'gap-29', '4px'], + ['gap-[calc(7rem+0.25rem)]', 'gap-29', '4px'], + ['gap-[116px]', 'gap-29', '4px'], + + // Non-suggested numbers, with a different spacing scale + ['gap-[7.25rem]', 'gap-116', '1px'], + ['gap-[calc(7rem+0.25rem)]', 'gap-116', '1px'], + ['gap-[116px]', 'gap-116', '1px'], + // Keep arbitrary value if units are different - ['w-[124px]', 'w-[124px]', '0.25rem'], + ['w-[124px]', 'w-31', '0.25rem'], // Keep arbitrary value if bare value doesn't fit in steps of .25 ['w-[0.123rem]', 'w-[0.123rem]', '0.25rem'], From 96ec73650f978471fc4a9e9149d726cc81e16b3b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 26 Nov 2025 11:36:57 +0100 Subject: [PATCH 2/5] try to convert any incoming arbitrary utility to a bare value To do this, we check if the `rem` option is provided, then we canonicalize the incoming value. We also canonicalize the spacing multiplier so we can deal with both `--spacing: 0.25rem` as `--spacing: 4px` values (different units). We then compute the bare value, and just try it as a replacement. --- .../src/canonicalize-candidates.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 13c3bc257a91..1a34a22ca9ba 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -990,6 +990,40 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO candidate.kind === 'arbitrary' ? candidate.value : (candidate.value?.value ?? null) if (value === null) return + // Try to canonicalize any incoming arbitrary value. Canonicalization of + // `rem` and `px` values will be converted to `px`, so we have to + // canonicalize the spacing multiplier as well. + if ( + options.signatureOptions.rem !== null && + candidate.kind === 'functional' && + candidate.value?.kind === 'arbitrary' + ) { + let spacingMultiplier = designSystem.resolveThemeValue('--spacing') + if (spacingMultiplier !== undefined) { + // Canonicalizing the spacing multiplier allows us to handle both + // `--spacing: 0.25rem` and `--spacing: 4px` values correctly. + let canonicalizedSpacingMultiplier = constantFoldDeclaration( + spacingMultiplier, + options.signatureOptions.rem, + ) + if (canonicalizedSpacingMultiplier !== null) { + let canonicalizedValue = constantFoldDeclaration(value, options.signatureOptions.rem) + let valueDimension = dimensions.get(canonicalizedValue) + let spacingMultiplierDimension = dimensions.get(canonicalizedSpacingMultiplier) + if ( + valueDimension && + spacingMultiplierDimension && + valueDimension[1] === spacingMultiplierDimension[1] // Ensure the units match + ) { + let bareValue = `${valueDimension[0] / spacingMultiplierDimension[0]}` + yield Object.assign({}, candidate, { + value: { kind: 'named', value: bareValue, fraction: null }, + }) + } + } + } + } + let spacingMultiplier = designSystem.storage[SPACING_KEY]?.get(value) ?? null let rootPrefix = '' if (spacingMultiplier !== null && spacingMultiplier < 0) { From 4bd24e9139628e2147965cc563ed44321139594e Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 26 Nov 2025 11:45:37 +0100 Subject: [PATCH 3/5] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61eeebe95b03..3a497ac4a1d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Preserve case of theme keys from JS configs and plugins ([#19337](https://github.com/tailwindlabs/tailwindcss/pull/19337)) - Write source maps correctly on the CLI when using `--watch` ([#19373](https://github.com/tailwindlabs/tailwindcss/pull/19373)) - Upgrade: Handle `future` and `experimental` config keys ([#19344](https://github.com/tailwindlabs/tailwindcss/pull/19344)) +- Try to canonicalize any arbitrary utility to a bare value ([#19379](https://github.com/tailwindlabs/tailwindcss/pull/19379)) ### Added From 7171d5ddf333aa8bf186035989d70b4662c79d4a Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 26 Nov 2025 12:00:57 +0100 Subject: [PATCH 4/5] only try the bare value if it's a valid spacing multiplier --- packages/tailwindcss/src/canonicalize-candidates.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 1a34a22ca9ba..0881e481168c 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -1016,9 +1016,11 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO valueDimension[1] === spacingMultiplierDimension[1] // Ensure the units match ) { let bareValue = `${valueDimension[0] / spacingMultiplierDimension[0]}` - yield Object.assign({}, candidate, { - value: { kind: 'named', value: bareValue, fraction: null }, - }) + if (isValidSpacingMultiplier(bareValue)) { + yield Object.assign({}, candidate, { + value: { kind: 'named', value: bareValue, fraction: null }, + }) + } } } } From 1380b876f0115186c50dbd5129572e1e8d362ec8 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 26 Nov 2025 12:01:36 +0100 Subject: [PATCH 5/5] ensure division by 0 never happens Probably will never happen in the real world because a spacing multiplier of 0px (or equivalent) doesn't make sense. --- packages/tailwindcss/src/canonicalize-candidates.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 0881e481168c..69f50c51f96e 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -1013,7 +1013,8 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO if ( valueDimension && spacingMultiplierDimension && - valueDimension[1] === spacingMultiplierDimension[1] // Ensure the units match + valueDimension[1] === spacingMultiplierDimension[1] && // Ensure the units match + spacingMultiplierDimension[0] !== 0 ) { let bareValue = `${valueDimension[0] / spacingMultiplierDimension[0]}` if (isValidSpacingMultiplier(bareValue)) {