Skip to content

Commit 8c77989

Browse files
authored
Ensure math operators are surrounded by whitespace in arbitrary values (#20011)
This PR fixes an issue where some `calc(…)` expressions become invalid after we canonicalize them to a different syntax. Let's say you start with: `left-[calc(-1*(var(--my-var1)+var(--my-var2)))]`, then the produced AST for this candidate looks like this: ```js [ { "kind": "functional", "root": "left", "modifier": null, "value": { "kind": "arbitrary", "dataType": null, "value": "calc(-1 * (var(--my-var1) + var(--my-var2)))" }, "variants": [], "important": false, "raw": "left-[calc(-1*(var(--my-var1)+var(--my-var2)))]" } ] ``` Notice that the `+` in between the vars already contain spaces. And the generated CSS looks like this: ```css .left-\[calc\(-1\*\(var\(--my-var1\)\+var\(--my-var2\)\)\)\] { left: calc(-1 * (var(--my-var1) + var(--my-var2))); } ``` Again, the `+` has spaces aroudn it. However, we canoncialize this syntax where we remove the `calc(-1 * <value>)` and move the `-` in front: `-left-[(var(--my-var1)+var(--my-var2))]`, which should behave the same, but it did not. The parsed value for this looks like: ```js [ { "kind": "functional", "root": "-left", "modifier": null, "value": { "kind": "arbitrary", "dataType": null, "value": "(var(--my-var1)+var(--my-var2))" }, "variants": [], "important": false, "raw": "-left-[(var(--my-var1)+var(--my-var2))]" } ] ``` Notice that the `+` does not contain spaces around it. That's because we add them when we parse arbitrary values and when we are in a `calc(…)` expression. But the `calc(<value> * -1)` is added later, so at this point, no spaces are added yet. This also means that the generated CSS currently looks like: ```css .-left-\[\(var\(--my-var1\)\+var\(--my-var2\)\)\] { left: calc((var(--my-var1)+var(--my-var2)) * -1); } ``` Which is invalid. To solve this, we will make sure to add the whitespace around operators when we re-insert the `calc(<value> * -1)`. With this fix, the CSS looks like this: ```css .-left-\[\(var\(--my-var1\)\+var\(--my-var2\)\)\] { left: calc((var(--my-var1) + var(--my-var2)) * -1); } ``` Which is correct again. --- There are a few other issues that need a bit more work, but are not required for this fix. 1. Can we get rid of the additional `(` and `)` parens? E.g.: ```diff - left-[calc(-1*(var(--my-var1)+var(--my-var2)))] - -left-[(var(--my-var1)+var(--my-var2))] + -left-[var(--my-var1)+var(--my-var2)] ``` Right now this means that we should use `calc((<value>) * -1)` instead of `calc(<value> * -1)` since `<value>` can be an expression on its own. This could lead to unwanted behavior, but this will be a follow up PR _if_ it's worth it. 2. Why did we even allow this canoncialization from A to B if it's not the same result? This last question is a bit more scary, but it has to do with how we compare results. We create a signature where we normalize a bunch of values to make sure that we can compare them. As a silly example `calc(var(--a) + var(--b))` and `calc(var(--b) + var(--a))` will result in the same value, therefor should have the same signature and should be swappable. But what's happening is that the signature of `left-[calc(-1*(var(--my-var1)+var(--my-var2)))]` and `-left-[(var(--my-var1)+var(--my-var2))]` result in: ``` /* Signature of: left-[calc(-1*(var(--my-var1)+var(--my-var2)))] */ .x { left: calc((var(--my-var1)+var(--my-var2))*-1); } /* Signature of: -left-[(var(--my-var1)+var(--my-var2))] */ .x { left: calc((var(--my-var1)+var(--my-var2))*-1); } ``` This gets rid of whitespace to store less data, but this is obviously wrong now, so we need to improve the signatures around this. That said, that will be a follow up PR as well because this requires much more testing. But this PR at least fixes the #20010 issue because we will properly insert the whitespace around the math operators. Fixes: #20010 ## Test plan 1. Added a regression test to make sure that the new canonicalized syntax results in the correct CSS. Before the fix, the test would fail: <img width="623" height="106" alt="image" src="https://github.com/user-attachments/assets/c470ce38-c3fa-4080-92ca-8a3e509f700b" /> 2. Existing tests still pass
1 parent b4db3b9 commit 8c77989

3 files changed

Lines changed: 13 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3030
- Export missing `PluginWithConfig` type from `tailwindcss/plugin` to fix errors when inferring plugin config types ([#19707](https://github.com/tailwindlabs/tailwindcss/pull/19707))
3131
- Ensure `start` and `end` legacy utilities without values do not generate CSS ([#20003](https://github.com/tailwindlabs/tailwindcss/pull/20003))
3232
- Ensure `--value(…)` is required in functional `@utility` definitions ([#20005](https://github.com/tailwindlabs/tailwindcss/pull/20005))
33+
- Canonicalization: preserve required whitespace around operators in negated arbitrary values (e.g. `-left-[(var(--a)+var(--b))]`) ([#20011](https://github.com/tailwindlabs/tailwindcss/pull/20011))
3334

3435
## [4.2.4] - 2026-04-21
3536

packages/tailwindcss/src/utilities.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,9 @@ test('left', async () => {
11091109
'left-4',
11101110
'-left-4',
11111111
'left-[4px]',
1112+
1113+
// https://github.com/tailwindlabs/tailwindcss/issues/20010
1114+
'-left-[(var(--my-var1)+var(--my-var2))]',
11121115
],
11131116
),
11141117
).toMatchInlineSnapshot(`
@@ -1121,6 +1124,10 @@ test('left', async () => {
11211124
left: calc(var(--spacing-4) * -1);
11221125
}
11231126

1127+
.-left-\\[\\(var\\(--my-var1\\)\\+var\\(--my-var2\\)\\)\\] {
1128+
left: calc((var(--my-var1) + var(--my-var2)) * -1);
1129+
}
1130+
11241131
.-left-full {
11251132
left: -100%;
11261133
}

packages/tailwindcss/src/utilities.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
isValidOpacityValue,
2525
isValidSpacingMultiplier,
2626
} from './utils/infer-data-type'
27+
import { addWhitespaceAroundMathOperators } from './utils/math-operators'
2728
import { replaceShadowColors } from './utils/replace-shadow-colors'
2829
import { segment } from './utils/segment'
2930
import * as ValueParser from './value-parser'
@@ -450,7 +451,10 @@ export function createUtilities(theme: Theme) {
450451
if (value === null) return
451452

452453
// Negate the value if the candidate has a negative prefix.
453-
return desc.handle(negative ? `calc(${value} * -1)` : value, dataType)
454+
return desc.handle(
455+
negative ? addWhitespaceAroundMathOperators(`calc(${value} * -1)`) : value,
456+
dataType,
457+
)
454458
}
455459
}
456460

0 commit comments

Comments
 (0)