From f8c8b77f61cab1ec24cbcaa2d0e15f336be6f34f Mon Sep 17 00:00:00 2001 From: Yeom-JinHo Date: Thu, 23 Oct 2025 23:34:55 +0900 Subject: [PATCH] fix: preserve arbitrary values using CSS variables during Tailwind v4 migration --- .../src/canonicalize-candidates.ts | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index 8df31af09091..6ae864f1b2d8 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -586,6 +586,13 @@ const spacing = new DefaultMap | }) function arbitraryUtilities(candidate: Candidate, options: SignatureOptions): Candidate { + // Add guard: Skip conversion if candidate uses CSS variables or complex expressions (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 + } + } // We are only interested in arbitrary properties and arbitrary values if ( // Arbitrary property @@ -673,6 +680,12 @@ function arbitraryUtilities(candidate: Candidate, options: SignatureOptions): Ca let value = candidate.kind === 'arbitrary' ? candidate.value : (candidate.value?.value ?? null) if (value === null) return + + // Always cast value to string for consistency + const valueStr = String(value) + + // Detect complex values (var(), calc(), spaces, commas, slashes) + const isComplex = /var\(--[^)]+\)|calc\(|\s|,|\//.test(valueStr) let spacingMultiplier = spacing.get(designSystem)?.get(value) ?? null let rootPrefix = '' @@ -689,19 +702,20 @@ function arbitraryUtilities(candidate: Candidate, options: SignatureOptions): Ca )) { if (rootPrefix) root = `${rootPrefix}${root}` - // Try as bare value - for (let replacementCandidate of parseCandidate(designSystem, `${root}-${value}`)) { - yield replacementCandidate - } - - // Try as bare value with modifier - if (candidate.modifier) { - for (let replacementCandidate of parseCandidate( - designSystem, - `${root}-${value}${candidate.modifier}`, - )) { + // Skip bare-value attempts for complex values to prevent gap-(--gap) corruption + if (!isComplex) { + for (let replacementCandidate of parseCandidate(designSystem, `${root}-${valueStr}`)) { yield replacementCandidate } + // Always use printModifier() for modifiers + if (candidate.modifier) { + for (let replacementCandidate of parseCandidate( + designSystem, + `${root}-${valueStr}${printModifier(candidate.modifier)}`, + )) { + yield replacementCandidate + } + } } // Try bare value based on the `--spacing` value. E.g.: @@ -726,16 +740,14 @@ function arbitraryUtilities(candidate: Candidate, options: SignatureOptions): Ca } } - // Try as arbitrary value - for (let replacementCandidate of parseCandidate(designSystem, `${root}-[${value}]`)) { + // Bracketed arbitrary values are always safe — keep them + for (let replacementCandidate of parseCandidate(designSystem, `${root}-[${valueStr}]`)) { yield replacementCandidate } - - // Try as arbitrary value with modifier if (candidate.modifier) { for (let replacementCandidate of parseCandidate( designSystem, - `${root}-[${value}]${printModifier(candidate.modifier)}`, + `${root}-[${valueStr}]${printModifier(candidate.modifier)}`, )) { yield replacementCandidate }