From 9e436f7751e7f68d7bd44d28c4f58293ae0a4c7e Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 26 Nov 2025 12:07:21 +0100 Subject: [PATCH] Try to canonicalize any arbitrary utility to a bare value (#19379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds an improvement to our canonicalization logic when dealing with arbitrary values. When trying to canonicalize utilities, we make use of the intellisense suggestions list where we typically use multiples of the spacing scale. This means that a value like `gap-[128px]` gets properly canonicalized to `gap-32`. However, when you try a value that we typically don't suggest such as `gap-[116px]` then it doesn't get canonicalized at all. This PR fixes that by trying to use the spacing scale and convert `116px / 4px` and try the `gap-29` utility instead. This is done by canonicalizing the incoming arbitrary value and the spacing multipliers such that `--spacing: 0.25rem` and `--spacing: 4px` both work as expected. ### Test plan 1. Added some tests with a spacing scale of `0.25rem` (which is the default) 2. Added some tests with the same spacing scale in a different unit `4px` 3. Added some tests with a different spacing scale `1px` Also had to update 1 test that now gets canonicalized properly, e.g.: `w-[124px]` → `w-31`. --- CHANGELOG.md | 1 + .../src/canonicalize-candidates.test.ts | 18 ++++++++- .../src/canonicalize-candidates.ts | 37 +++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61eeebe95..3a497ac4a 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 diff --git a/packages/tailwindcss/src/canonicalize-candidates.test.ts b/packages/tailwindcss/src/canonicalize-candidates.test.ts index 26a34b964..fd5826c4d 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'], diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 13c3bc257..69f50c51f 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -990,6 +990,43 @@ 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 + spacingMultiplierDimension[0] !== 0 + ) { + let bareValue = `${valueDimension[0] / spacingMultiplierDimension[0]}` + if (isValidSpacingMultiplier(bareValue)) { + 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) {