Try to canonicalize any arbitrary utility to a bare value (#19379)

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`.
This commit is contained in:
Robin Malfait 2025-11-26 12:07:21 +01:00 • committed by GitHub
parent 117433a100
commit 9e436f7751
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 55 additions and 1 deletions

View file

@ -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

View file

@ -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'],

View file

@ -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) {