From 1630fd043cbc29ebaf294ec3b17f8e9fea33b923 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 19 Mar 2026 13:20:20 +0100 Subject: [PATCH 1/4] add failing test --- .../src/canonicalize-candidates.test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/tailwindcss/src/canonicalize-candidates.test.ts b/packages/tailwindcss/src/canonicalize-candidates.test.ts index 4b6ecbfd6c3c..dd7038e7a120 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.test.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.test.ts @@ -1120,6 +1120,32 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', await expectCombinedCanonicalization(input, candidates.trim(), expected) }) }) + + // https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1558 + test.each([ + ['tracking-[-0.05em]', 'tracking-tighter'], + ['tracking-[-0.025em]', 'tracking-tight'], + ['tracking-[0em]', 'tracking-normal'], + ['tracking-[0.025em]', 'tracking-wide'], + ['tracking-[0.05em]', 'tracking-wider'], + ['tracking-[0.1em]', 'tracking-widest'], + ])(testName, { timeout }, async (candidate, expected) => { + await expectCanonicalization( + css` + @import 'tailwindcss'; + @theme { + --tracking-tighter: -0.05em; + --tracking-tight: -0.025em; + --tracking-normal: 0em; + --tracking-wide: 0.025em; + --tracking-wider: 0.05em; + --tracking-widest: 0.1em; + } + `, + candidate, + expected, + ) + }) }) describe('theme to var', () => { From 595f639b7aad3fb940bc83db82d191f3174068b3 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 19 Mar 2026 13:28:50 +0100 Subject: [PATCH 2/4] add tests for negative tracking utilities that don't make much sense --- packages/tailwindcss/src/canonicalize-candidates.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/tailwindcss/src/canonicalize-candidates.test.ts b/packages/tailwindcss/src/canonicalize-candidates.test.ts index dd7038e7a120..fef9eee08008 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.test.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.test.ts @@ -1129,6 +1129,14 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', ['tracking-[0.025em]', 'tracking-wide'], ['tracking-[0.05em]', 'tracking-wider'], ['tracking-[0.1em]', 'tracking-widest'], + + // Negative values that don't make sense + // See: https://tailwindcss.com/docs/letter-spacing#using-negative-values + ['-tracking-tighter', 'tracking-wider'], + ['-tracking-tight', 'tracking-wide'], + ['-tracking-normal', 'tracking-normal'], + ['-tracking-wide', 'tracking-tight'], + ['-tracking-wider', 'tracking-tighter'], ])(testName, { timeout }, async (candidate, expected) => { await expectCanonicalization( css` From fcc81f95739cc7470045b79be52ac7905c1e2b3f Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 19 Mar 2026 17:58:58 +0100 Subject: [PATCH 3/4] prefer positive candidates over negative candidates Right now, when we _know_ that a replacement utility exist, then we use it immediately. However, if multiple match then we don't really know what to do so up until now we just bailed. But the funny part is that `-tracking-wide` and `tracking-tight` result in the exact same signature. In this situation, we pick the positive value instead of the negative value. If at any point, multiple positive utilities exist, we bail again. So this only covers the above scenario where a negative and positive utility was present. --- .../src/canonicalize-candidates.ts | 56 +++++++++++++++---- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 566b4a3f76b2..eaa055b25dd4 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -1128,11 +1128,29 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO // Find a corresponding utility for the same signature let replacements = utilities.get(targetSignature) - // Multiple utilities can map to the same signature. Not sure how to migrate - // this one so let's just skip it for now. - // - // TODO: Do we just migrate to the first one? - if (replacements.length > 1) return + // Multiple utilities can map to the same signature. + if (replacements.length > 1) { + // Prefer positive values over negative values + let maybeReplacement: string | undefined = undefined + for (let replacement of replacements) { + if (replacement[0] === '-') continue // Skip negative values + + // If multiple non-negative replacements exists then we are unsure + // what to do, so let's bail. + if (maybeReplacement) return + + // Consider this replacement + maybeReplacement = replacement + } + + if (maybeReplacement) { + for (let replacementCandidate of parseCandidate(designSystem, maybeReplacement)) { + yield replacementCandidate + } + } + + return + } // If we didn't find any replacement utilities, let's try to strip the // 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 // Find a corresponding utility for the same signature let replacements = utilities.get(targetSignature) - // Multiple utilities can map to the same signature. Not sure how to migrate - // this one so let's just skip it for now. - // - // TODO: Do we just migrate to the first one? - if (replacements.length > 1) return + // Multiple utilities can map to the same signature. + if (replacements.length > 1) { + // Prefer positive values over negative values + let maybeReplacement: string | undefined = undefined + for (let replacement of replacements) { + if (replacement[0] === '-') continue // Skip negative values + + // If multiple non-negative replacements exists then we are unsure + // what to do, so let's bail. + if (maybeReplacement) return + + // Consider this replacement + maybeReplacement = replacement + } + + if (maybeReplacement) { + for (let replacementCandidate of parseCandidate(designSystem, maybeReplacement)) { + yield replacementCandidate + } + } + + return + } // If we didn't find any replacement utilities, let's try to strip the // modifier and find a replacement then. If we do, we can try to re-add the From 5b1c46efff91f1beb6cd67573c902096c7195091 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 19 Mar 2026 19:52:49 +0100 Subject: [PATCH 4/4] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9c103bc64a1..3b639aed9009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - _Experimental_: Add `@container-size` utility ([#18901](https://github.com/tailwindlabs/tailwindcss/pull/18901)) +### Fixed + +- Improve canonicalizations for `tracking-*` utilities ([#19827](https://github.com/tailwindlabs/tailwindcss/pull/19827)) + ## [4.2.2] - 2026-03-18 ### Fixed