From dfca08fe6ad2968ab07e796d93e6e82fcc54c723 Mon Sep 17 00:00:00 2001 From: Yeom-JinHo Date: Thu, 23 Oct 2025 23:47:13 +0900 Subject: [PATCH 1/2] fix: preserve arbitrary values using CSS variables during Tailwind v4 migration --- .../src/canonicalize-candidates.ts | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 13c3bc257a91..fed18c1b88ea 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -912,6 +912,14 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO return candidate } + // Guard — skip when value contains var()/calc()/spaces/commas/slashes + if (candidate.kind === 'functional' && candidate.value?.kind === 'arbitrary') { + const raw = String(candidate.value.value ?? '') + if (/var\(--[^)]+\)|calc\(|\s|,|\//.test(raw)) { + return candidate + } + } + let designSystem = options.designSystem let utilities = designSystem.storage[PRE_COMPUTED_UTILITIES_KEY].get(options.signatureOptions) let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions) @@ -997,6 +1005,10 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO spacingMultiplier = Math.abs(spacingMultiplier) } + // normalize and detect complex values + const valueStr = String(value) + const isComplex = /var\(--[^)]+\)|calc\(|\s|,|\//.test(valueStr) + for (let root of Array.from(designSystem.utilities.keys('functional')).sort( // Sort negative roots after positive roots so that we can try // `mt-*` before `-mt-*`. This is especially useful in situations where @@ -1006,17 +1018,23 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO if (rootPrefix) root = `${rootPrefix}${root}` // Try as bare value - for (let replacementCandidate of parseCandidate(designSystem, `${root}-${value}`)) { - yield replacementCandidate + // ✅ changed: skip bare value for complex values; use valueStr + if (!isComplex) { + for (let replacementCandidate of parseCandidate(designSystem, `${root}-${valueStr}`)) { + yield replacementCandidate + } } // Try as bare value with modifier if (candidate.modifier) { - for (let replacementCandidate of parseCandidate( - designSystem, - `${root}-${value}${candidate.modifier}`, - )) { - yield replacementCandidate + // guard complex & use printModifier + valueStr + if (!isComplex) { + for (let replacementCandidate of parseCandidate( + designSystem, + `${root}-${valueStr}${printModifier(candidate.modifier)}` + )) { + yield replacementCandidate + } } } @@ -1043,7 +1061,8 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO } // Try as arbitrary value - for (let replacementCandidate of parseCandidate(designSystem, `${root}-[${value}]`)) { + // ✅ changed: use valueStr to keep normalized/bracketed form + for (let replacementCandidate of parseCandidate(designSystem, `${root}-[${valueStr}]`)) { // ✅ changed yield replacementCandidate } @@ -1051,7 +1070,7 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO if (candidate.modifier) { for (let replacementCandidate of parseCandidate( designSystem, - `${root}-[${value}]${printModifier(candidate.modifier)}`, + `${root}-[${valueStr}]${printModifier(candidate.modifier)}`, // (kept) uses printModifier )) { yield replacementCandidate } From 985f67e3a32af7ea628c8c061680a34137659398 Mon Sep 17 00:00:00 2001 From: Yeom-JinHo Date: Fri, 24 Oct 2025 00:02:08 +0900 Subject: [PATCH 2/2] fix: refine arbitrary value conversion guard to allow theme() functions --- packages/tailwindcss/src/canonicalize-candidates.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index fed18c1b88ea..d45793ca6c1a 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -912,10 +912,13 @@ function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeO return candidate } - // Guard — skip when value contains var()/calc()/spaces/commas/slashes + // Guard — skip when value contains complex expressions that shouldn't be converted + // Allow theme() functions and color: data types, but block complex var() expressions if (candidate.kind === 'functional' && candidate.value?.kind === 'arbitrary') { const raw = String(candidate.value.value ?? '') - if (/var\(--[^)]+\)|calc\(|\s|,|\//.test(raw)) { + // Skip if it contains calc() or complex var() expressions (with spaces, commas, slashes) + // But allow simple var(--color-name) patterns from theme() functions + if (/calc\(|\s|,|\//.test(raw) && !/theme\(|color:/.test(raw)) { return candidate } }