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 @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix crash in canonicalization step when handling utilities with empty property maps ([#19727](https://github.com/tailwindlabs/tailwindcss/pull/19727))
- Skip full reload for server only modules scanned by client CSS when using `@tailwindcss/vite` ([#19745](https://github.com/tailwindlabs/tailwindcss/pull/19745))
- Add support for Vite 8 in `@tailwindcss/vite` ([#19790](https://github.com/tailwindlabs/tailwindcss/pull/19790))
- Improve canonicalization for bare values exceeding default spacing scale suggestions (e.g. `w-1234 h-1234` → `size-1234`) ([#19809](https://github.com/tailwindlabs/tailwindcss/pull/19809))

## [4.2.1] - 2026-02-23

Expand Down
7 changes: 7 additions & 0 deletions packages/tailwindcss/src/canonicalize-candidates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1054,6 +1054,13 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
// To completely different utility
['w-4 h-4', 'size-4'],

// Goes beyond the default spacing scale that's being used in intellisense
// for code completion. Since it's about bare values, we should still be
// able to combine them.
['w-123 h-123', 'size-123'],
['w-128 h-128', 'size-128'], // `w-128` on its own would become `w-lg`
['mt-123 mb-123', 'my-123'],

// Do not touch if not operating on the same variants
['hover:w-4 h-4', 'hover:w-4 h-4'],

Expand Down
48 changes: 47 additions & 1 deletion packages/tailwindcss/src/canonicalize-candidates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,12 +312,52 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st
}
}

let dynamicUtilities = new DefaultMap((candidate: string) => {
let result = new DefaultMap(
(_property: string) => new DefaultMap((_value: string) => new Set<string>()),
)

let relevantProperties = new Set(computeUtilitiesPropertiesLookup.get(candidate).keys())
if (relevantProperties.size === 0) return result

for (let parsedCandidate of parseCandidate(designSystem, candidate)) {
if (
parsedCandidate.kind !== 'functional' ||
parsedCandidate.value?.kind !== 'named' // Necessary for bare values
) {
continue
}

for (let root of designSystem.utilities.keys('functional')) {
if (root === parsedCandidate.root) continue // Skip self

let replacement = printUnprefixedCandidate(designSystem, {
...cloneCandidate(parsedCandidate),
root,
})

let propertyValues = computeUtilitiesPropertiesLookup.get(replacement)
for (let [property, values] of propertyValues) {
if (!relevantProperties.has(property)) continue // Skip properties that are not relevant for the current candidate

for (let value of values) {
result.get(property).get(value).add(replacement)
}
}
}

return result
}

return result
})

// For each property, lookup other utilities that also set this property and
// this exact value. If multiple properties are used, use the intersection of
// each property.
//
// E.g.: `margin-top` → `mt-1`, `my-1`, `m-1`
let otherUtilities = candidatePropertiesValues.map((propertyValues) => {
let otherUtilities = candidatePropertiesValues.map((propertyValues, idx) => {
let result: Set<string> | null = null
for (let property of propertyValues.keys()) {
let otherUtilities = new Set<string>()
Expand All @@ -327,6 +367,12 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st
}
}

for (let value of propertyValues.get(property)) {
for (let candidate of dynamicUtilities.get(candidates[idx]).get(property).get(value)) {
otherUtilities.add(candidate)
}
}

if (result === null) result = otherUtilities
else result = intersection(result, otherUtilities)

Expand Down