[csswg-drafts] [css-values-4] Calc simplification of slightly non-trivial products (#13902)

tabatkins has just created a new issue for https://github.com/w3c/csswg-drafts:

== [css-values-4] Calc simplification of slightly non-trivial products ==
While looking at #13069 (a PR adding another simplification case for calc()), I realized that the WPT linked in the comment actually depends on *another* simplification that Chrome does that isn't in the spec, which all three major browsers do *differently*. So we need to decide which way we actually want it to work.

The question is what happens when you have a Product of two values, one of which is a number and the second is a non-trivial Sum. For example:

`1 * (min(10px, 20%) + 30px)`

The [simplification algorithm](https://drafts.csswg.org/css-values-4/#calc-simplification) has a few cases for simplifying Products. In particular, Step 9.3 *almost* applies: it simplifies cases like `2 * (10px + 20%)` into `20px + 40%`, which contain just a number and a Sum. But it requires the Sum to be only numeric values; if it contains anything else (like a `min()` function), it doesn't apply per spec.

Here are a few variations on this theme across browsers. All three browsers *agree* on cases 2, 3, and 5. Chrome and Firefox agree on case 1, while Safari disagrees. All three browsers agree on case 5.

<details><summary><a href="https://software.hixie.ch/utilities/js/live-dom-viewer/saved/14712">LiveDomViewer link</a></summary>
<pre>
&lt;!DOCTYPE html>
&lt;body>
&lt;script>
function test(val) {
 val = "calc(" + val + ")";
 document.body.style.marginLeft = val;
 w(`input:  ${val}`);
 w(`parsed: ${document.body.style.marginLeft}`);
 //w(`gCS():  ${getComputedStyle(document.body).marginLeft}`);
 w("==========")
}
test("1 * (min(10px, 20%) + 30px)");
test("-1 * (min(10px, 20%) + 30px)");
test("2 * (min(10px, 20%) + 30px)");
test("0px - 1 * (min(10px, 20%) + 30px)");
test("0px - 2 * (min(10px, 20%) + 30px)");
&lt;/script>
</pre>
</details>

The agreeing cases are all correct, per spec. For cases 2 and 5, Safari is correct per spec, while both Chrome and Safari do something different.

* In Case 1, the Product is `1 * ...`, and both Chrome and Firefox just return the `...`. (If the number is anything else, `-1` or `2`, they don't do that, and match Safari, as you can see in cases 2 and 3.)
* In case 4, same thing, producing `0px - (min(10px, 20%) + 30px)`. Firefox then additionally does a Negate simplification, turning the `30px` into `-30px` *and the `min(10px, 20%)` into a `max(-10px, -20%)`*! That's definitely not in the spec (but I think it's a *valid* simplification to perform).

# Chrome
```
log: input:  calc(1 * (min(10px, 20%) + 30px))
log: parsed: calc(30px + min(10px, 20%))
log: ==========
log: input:  calc(-1 * (min(10px, 20%) + 30px))
log: parsed: calc(-1 * (30px + min(10px, 20%)))
log: ==========
log: input:  calc(2 * (min(10px, 20%) + 30px))
log: parsed: calc(2 * (30px + min(10px, 20%)))
log: ==========
log: input:  calc(0px - 1 * (min(10px, 20%) + 30px))
log: parsed: calc(30px - min(10px, 20%))
log: ==========
log: input:  calc(0px - 2 * (min(10px, 20%) + 30px))
log: parsed: calc(0px - (2 * (30px + min(10px, 20%))))
```

# Firefox
```
log: input:  calc(1 * (min(10px, 20%) + 30px))
log: parsed: calc(30px + min(10px, 20%))
log: ==========
log: input:  calc(-1 * (min(10px, 20%) + 30px))
log: parsed: calc(-1 * (30px + min(10px, 20%)))
log: ==========
log: input:  calc(2 * (min(10px, 20%) + 30px))
log: parsed: calc(2 * (30px + min(10px, 20%)))
log: ==========
log: input:  calc(0px - 1 * (min(10px, 20%) + 30px))
log: parsed: calc(-30px + max(-10px, -20%))
log: ==========
log: input:  calc(0px - 2 * (min(10px, 20%) + 30px))
log: parsed: calc(0px - (2 * (30px + min(10px, 20%))))
```

# WebKit
```
log: input:  calc(1 * (min(10px, 20%) + 30px))
log: parsed: calc(1 * (30px + min(10px, 20%)))
log: ==========
log: input:  calc(-1 * (min(10px, 20%) + 30px))
log: parsed: calc(-1 * (30px + min(10px, 20%)))
log: ==========
log: input:  calc(2 * (min(10px, 20%) + 30px))
log: parsed: calc(2 * (30px + min(10px, 20%)))
log: ==========
log: input:  calc(0px - 1 * (min(10px, 20%) + 30px))
log: parsed: calc(0px - (1 * (30px + min(10px, 20%))))
log: ==========
log: input:  calc(0px - 2 * (min(10px, 20%) + 30px))
log: parsed: calc(0px - (2 * (30px + min(10px, 20%))))
```


============

So, questions: 

1. Do we want to accept this `1 * ...` => `...` simplification that Chrome and Firefox do?
2. Do we want to accept this `- min(X, Y)` => `max(-X, -Y)` simplification that Firefox does?

/cc @emilio @weinig @lilles 

Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/13902 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Friday, 8 May 2026 19:28:25 UTC