No description
Find a file
Robin Malfait 8c779899bb
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
2026-05-05 17:08:24 +02:00
.github Improve CI for NAPI-RS (#19983) 2026-04-26 19:37:28 +02:00
crates docs: fix various typos in comments and documentation (#19878) 2026-04-29 13:28:39 +00:00
integrations Improve codebase quality (#19999) 2026-04-30 23:32:37 +02:00
packages Ensure math operators are surrounded by whitespace in arbitrary values (#20011) 2026-05-05 17:08:24 +02:00
patches Bump Lightning CSS (#19771) 2026-03-18 15:57:52 +01:00
playgrounds Bump dependencies in playgrounds (#19954) 2026-04-23 12:51:20 +02:00
scripts Make TypeScript a bit more happy (#19124) 2025-10-14 19:52:46 +00:00
.gitignore Fix slow unit test (#17465) 2025-03-31 15:26:01 +02:00
.prettierignore Bump dependencies (#19608) 2026-02-04 12:38:50 +01:00
Cargo.lock Bump NAPI related dependencies (#19982) 2026-04-26 17:49:06 +02:00
Cargo.toml Hoist oxide/crates to just crates (#13333) 2024-03-23 09:00:48 -04:00
CHANGELOG.md Ensure math operators are surrounded by whitespace in arbitrary values (#20011) 2026-05-05 17:08:24 +02:00
LICENSE Add README, LICENSE, and CONTRIBUTING (#13088) 2024-03-05 14:45:39 -05:00
package.json Bump dependencies (#19957) 2026-04-24 21:21:12 +02:00
pnpm-lock.yaml Update enhanced-resolve 5.20.1 → 5.21.0 (minor) (#19998) 2026-05-01 11:29:19 +00:00
pnpm-workspace.yaml Bump dependencies (#19957) 2026-04-24 21:21:12 +02:00
README.md docs: fix GitHub links to tailwindlabs org (#19686) 2026-02-17 13:06:49 +01:00
rust-toolchain.toml Bump NAPI related dependencies (#19982) 2026-04-26 17:49:06 +02:00
turbo.json Fix segmentation fault when loading @tailwindcss/oxide in a Worker thread (#17276) 2025-03-18 16:28:20 -04:00
vitest.config.ts Bump Vitest to v4 (#19216) 2025-11-20 18:16:20 -05:00

Tailwind CSS

A utility-first CSS framework for rapidly building custom user interfaces.

Build Status Total Downloads Latest Release License


Documentation

For full documentation, visit tailwindcss.com.

Community

For help, discussion about best practices, or feature ideas:

Discuss Tailwind CSS on GitHub

Contributing

If you're interested in contributing to Tailwind CSS, please read our contributing docs before submitting a pull request.