Skip to content

Commit 7482d47

Browse files
authored
Add canonicalizations for tracking-* utilities (#19827)
This PR adds support for canonicalizations for `tracking-*` utilities. This one is a bit of a funny one, if you take a look at the linked issue, there is a beautiful table: | Utility Name | Value | Arbitrary Value | Throws Suggestion | | - | -: | - | - | | tracking-tighter | -0.05em | tracking-[-0.05em] | ✗ | | tracking-tight | -0.025em | tracking-[-0.025em] | ✗ | | tracking-normal | 0em | tracking-[0em] | ✗ | | tracking-wide | 0.025em | tracking-[0.025em] | ✗ | | tracking-wider | 0.05em | tracking-[0.05em] | ✗ | | tracking-widest | 0.1em | tracking-[0.1em] | ✓ | It doesn't really make sense to _why_ only the `tracking-widest` one is properly suggested here. Until you look a little bit closer. Turns out that `-tracking-tighter` is equivalent to `tracking-wider`, `-tracking-tight` is equivalent to `tracking-wide` and so on. The way the canonicalization works internally is by generating a signature for a given utility class. If two utilities have the exact same signature, we can consider them the same. In this case `tracking-widest` and `tracking-[0.1em]` have the same signature. One of the rules we have internally is that if we find more than one replacement utility then we don't really know what to do, so we bail. Because if you get `foo` or `bar`, which one do you pick? If we refer to this above table again, the moment we want to canonicalize the `tracking-[-0.05em]` we get two suggestions: `tracking-tighter` and `-tracking-wider`, since we don't know what to do, we bail and we don't suggest anything. So the reason that `tracking-widest` _was_ suggested is just because we don't have a `-tracking-tightest`. How do we fix this? Well, since we have `tracking-*` and `-tracking-*` utilities, I wanted to deprecate the `-tracking-*` ones for named utilities (where the values come from your theme) because that doesn't really make sense. However, we have this exact pattern documented here: https://tailwindcss.com/docs/letter-spacing#using-negative-values Which means that I can't just deprecate those utilities. <img width="723" height="511" alt="image" src="https://github.com/user-attachments/assets/164b659b-abe9-4f6e-a176-701dd7ea505a" /> Instead, I added a different rule which says that if you get multiple possible replacements, then we prefer the "positive" one, the one without the `-`. Also added some additional checks to make sure that if you get `foo`, `-bar`, `baz`, that we also bail because we know that we should prefer `foo` or `baz` over `-bar`, but we don't know if we should pick `foo` or `baz`... This additional rule does solve the original issue, and we already prefer possible values over negative values in other places (related to bare values). Fixes: tailwindlabs/tailwindcss-intellisense#1558 ## Test plan 1. Existing tests pass 2. Added regression tests to make sure that the table from above _does_ get canonicalized correctly into the expected values.
1 parent d596b0c commit 7482d47

File tree

3 files changed

+84
-10
lines changed

3 files changed

+84
-10
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901))
1313

14+
### Fixed
15+
16+
- Improve canonicalizations for `tracking-*` utilities ([#19827](https://github.com/tailwindlabs/tailwindcss/pull/19827))
17+
1418
## [4.2.2] - 2026-03-18
1519

1620
### Fixed

packages/tailwindcss/src/canonicalize-candidates.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,40 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
11201120
await expectCombinedCanonicalization(input, candidates.trim(), expected)
11211121
})
11221122
})
1123+
1124+
// https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1558
1125+
test.each([
1126+
['tracking-[-0.05em]', 'tracking-tighter'],
1127+
['tracking-[-0.025em]', 'tracking-tight'],
1128+
['tracking-[0em]', 'tracking-normal'],
1129+
['tracking-[0.025em]', 'tracking-wide'],
1130+
['tracking-[0.05em]', 'tracking-wider'],
1131+
['tracking-[0.1em]', 'tracking-widest'],
1132+
1133+
// Negative values that don't make sense
1134+
// See: https://tailwindcss.com/docs/letter-spacing#using-negative-values
1135+
['-tracking-tighter', 'tracking-wider'],
1136+
['-tracking-tight', 'tracking-wide'],
1137+
['-tracking-normal', 'tracking-normal'],
1138+
['-tracking-wide', 'tracking-tight'],
1139+
['-tracking-wider', 'tracking-tighter'],
1140+
])(testName, { timeout }, async (candidate, expected) => {
1141+
await expectCanonicalization(
1142+
css`
1143+
@import 'tailwindcss';
1144+
@theme {
1145+
--tracking-tighter: -0.05em;
1146+
--tracking-tight: -0.025em;
1147+
--tracking-normal: 0em;
1148+
--tracking-wide: 0.025em;
1149+
--tracking-wider: 0.05em;
1150+
--tracking-widest: 0.1em;
1151+
}
1152+
`,
1153+
candidate,
1154+
expected,
1155+
)
1156+
})
11231157
})
11241158

11251159
describe('theme to var', () => {

packages/tailwindcss/src/canonicalize-candidates.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,11 +1128,29 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO
11281128
// Find a corresponding utility for the same signature
11291129
let replacements = utilities.get(targetSignature)
11301130

1131-
// Multiple utilities can map to the same signature. Not sure how to migrate
1132-
// this one so let's just skip it for now.
1133-
//
1134-
// TODO: Do we just migrate to the first one?
1135-
if (replacements.length > 1) return
1131+
// Multiple utilities can map to the same signature.
1132+
if (replacements.length > 1) {
1133+
// Prefer positive values over negative values
1134+
let maybeReplacement: string | undefined = undefined
1135+
for (let replacement of replacements) {
1136+
if (replacement[0] === '-') continue // Skip negative values
1137+
1138+
// If multiple non-negative replacements exists then we are unsure
1139+
// what to do, so let's bail.
1140+
if (maybeReplacement) return
1141+
1142+
// Consider this replacement
1143+
maybeReplacement = replacement
1144+
}
1145+
1146+
if (maybeReplacement) {
1147+
for (let replacementCandidate of parseCandidate(designSystem, maybeReplacement)) {
1148+
yield replacementCandidate
1149+
}
1150+
}
1151+
1152+
return
1153+
}
11361154

11371155
// If we didn't find any replacement utilities, let's try to strip the
11381156
// modifier and find a replacement then. If we do, we can try to re-add the
@@ -1353,11 +1371,29 @@ function bareValueUtilities(candidate: Candidate, options: InternalCanonicalizeO
13531371
// Find a corresponding utility for the same signature
13541372
let replacements = utilities.get(targetSignature)
13551373

1356-
// Multiple utilities can map to the same signature. Not sure how to migrate
1357-
// this one so let's just skip it for now.
1358-
//
1359-
// TODO: Do we just migrate to the first one?
1360-
if (replacements.length > 1) return
1374+
// Multiple utilities can map to the same signature.
1375+
if (replacements.length > 1) {
1376+
// Prefer positive values over negative values
1377+
let maybeReplacement: string | undefined = undefined
1378+
for (let replacement of replacements) {
1379+
if (replacement[0] === '-') continue // Skip negative values
1380+
1381+
// If multiple non-negative replacements exists then we are unsure
1382+
// what to do, so let's bail.
1383+
if (maybeReplacement) return
1384+
1385+
// Consider this replacement
1386+
maybeReplacement = replacement
1387+
}
1388+
1389+
if (maybeReplacement) {
1390+
for (let replacementCandidate of parseCandidate(designSystem, maybeReplacement)) {
1391+
yield replacementCandidate
1392+
}
1393+
}
1394+
1395+
return
1396+
}
13611397

13621398
// If we didn't find any replacement utilities, let's try to strip the
13631399
// modifier and find a replacement then. If we do, we can try to re-add the

0 commit comments

Comments
 (0)