From b8c334a199ae3f00c7ee8821220314ac276c4db0 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 7 Oct 2025 15:13:12 +0200 Subject: [PATCH 1/3] add failing tests --- packages/tailwindcss/src/canonicalize-candidates.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/tailwindcss/src/canonicalize-candidates.test.ts b/packages/tailwindcss/src/canonicalize-candidates.test.ts index 811de77f5dc0..0ee92a7d15b2 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.test.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.test.ts @@ -254,6 +254,10 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s', ['[color:var(--color-red-500)]/[25%]', 'text-red-500/25'], ['[color:var(--color-red-500)]/[100%]', 'text-red-500'], ['[color:var(--color-red-500)]/100', 'text-red-500'], + ['[color:var(--color-red-500)]/[10%]', 'text-red-500/10'], + ['[color:var(--color-red-500)]/[10.0%]', 'text-red-500/10'], + ['[color:var(--color-red-500)]/[.1]', 'text-red-500/10'], + ['[color:var(--color-red-500)]/[.10]', 'text-red-500/10'], // No need for `/50` because that's already encoded in the `--color-primary` // value ['[color:oklch(62.3%_0.214_259.815)]/50', 'text-primary'], From 0673b71185f8d8d019d24ebb4156f784d00829db Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 7 Oct 2025 15:13:19 +0200 Subject: [PATCH 2/3] try a normalized percentage If you have `10.0%` or `10%` they should be the same thing. So we pass the `10.0` and `10` through `Number` to normalize the value. We only have to do this for values containing a `.`. --- packages/tailwindcss/src/canonicalize-candidates.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/canonicalize-candidates.ts b/packages/tailwindcss/src/canonicalize-candidates.ts index ec07b9511f14..8f3312194bbf 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.ts @@ -1469,7 +1469,11 @@ function optimizeModifier(designSystem: DesignSystem, candidate: Candidate): Can { let newModifier: NamedUtilityValue = { kind: 'named', - value: modifier.value.endsWith('%') ? modifier.value.slice(0, -1) : modifier.value, + value: modifier.value.endsWith('%') + ? modifier.value.includes('.') + ? `${Number(modifier.value.slice(0, -1))}` + : modifier.value.slice(0, -1) + : modifier.value, fraction: null, } From 3b260adfec8d0ff3abd02ef8d93df6cf850d8d0d Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 7 Oct 2025 15:31:04 +0200 Subject: [PATCH 3/3] normalize percentages when computing the signature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We are only interested in percentages containing a `.`. This way we can normalize `10.0%` as `10%` by using `Number(value)` and re-attaching the `%`. With this, a `10.0%` and `10%` would be considered the same. The regex in theory could include a leading `[+-]` but that won't have an effect here, and could be even more confusing when it's being used in a `calc(…)` expression. --- packages/tailwindcss/src/signatures.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/tailwindcss/src/signatures.ts b/packages/tailwindcss/src/signatures.ts index 89a5edde424c..80d8f2948206 100644 --- a/packages/tailwindcss/src/signatures.ts +++ b/packages/tailwindcss/src/signatures.ts @@ -9,6 +9,8 @@ import { dimensions } from './utils/dimensions' import { isValidSpacingMultiplier } from './utils/infer-data-type' import * as ValueParser from './value-parser' +const FLOATING_POINT_PERCENTAGE = /\d*\.\d+(?:[eE][+-]?\d+)?%/g + // Given a utility, compute a signature that represents the utility. The // signature will be a normalised form of the generated CSS for the utility, or // a unique symbol if the utility is not valid. The class in the selector will @@ -59,6 +61,17 @@ export const computeUtilitySignature = new DefaultMap< if (node.value === undefined || node.property === '--tw-sort') { replaceWith([]) } + + // Normalize percentages by removing unnecessary dots and zeros. + // + // E.g.: `50.0%` → `50%` + else if (node.value.includes('%')) { + FLOATING_POINT_PERCENTAGE.lastIndex = 0 + node.value = node.value.replaceAll( + FLOATING_POINT_PERCENTAGE, + (match) => `${Number(match.slice(0, -1))}%`, + ) + } } // Replace special nodes with its children