Repository navigation
[css-values] Clarifications on how calc-mix() simplification should work #13839
Description
I starting to try and implement calc-mix() and I am not sure when its ok to simplify the function.
Let's use an example to make things explicit:
<!DOCTYPE html>
<html>
<body>
<pre id="output"></pre>
<div style="width: 200px">
<div id="test" style="text-indent: calc-mix(10px 50%, 20px)"></div>
</div>
<script>
const test = document.getElementById('test');
const output = document.getElementById('output');
output.textContent += `#test: specified = ${test.style["text-indent"]}\n`;
output.textContent += `#test: computed = ${getComputedStyle(test)["text-indent"]}\n`;
</script>
</body>
</html>This could print out something like:
#test: specified = calc-mix(10px 50%, 20px 50%)
#test: computed = 15px
(The 15px comes from evaluating the calc-mix like this: (10px * 0.5) + (20px * 0.5) = (5px + 10px) = 15px. Is this right? No where is the algorithm for evaluating calc-mix defined, it just states "The used value of a valid mix notation is the weighted average of its arguments" but doesn't define what it means by "weighted average").
Question 1: Should the specified value serialization include the normalized percentages (e.g. filled in the second 50%)?
Question 2: Should the specified value serialization be simplified down to just calc(15px) instead?
Making the example a bit more complicated, let's change the calc-mix to calc-mix(10em 50%, 20px). Now, we can't simplify at specified value time, since we can't resolve the em values.
Is the correct output now?
#test: specified = calc-mix(10em 50%, 20px 50%)
#test: computed = 90px
Question 3: Should the specified value serialization have been simplified down to products and sums (e.g. #test: specified = calc((10em * 0.5) + (20px * 0.5)))?
Going one step further, let's change the calc-mix to calc-mix(25% 50%, 20px). Now we can't simplify even at computed value time, since we can't resolve the percentage value.
Is the correct output now?
#test: specified = calc-mix(25% 50%, 20px 50%)
#test: computed = calc-mix(25% 50%, 20px 50%)
Finally, what about 0% items. Let's change the calc-mix to calc-mix(25% 50%, 20px, 300px 0%).
Is the correct output still?
#test: specified = calc-mix(25% 50%, 20px 50%)
#test: computed = calc-mix(25% 50%, 20px 50%)
Question 4: Should 0% terms be dropped from the specified and/or computed value serializations?
--
I think one thing that would make this (and future functions added to calc()'s repertoire) easier from an implementors standpoint is if there were a clearer way for functions to be defined. Ideally something like CSS Values 4 (the base calc() spec work) would define what a function needed to work in calc() via a set of named conformance hooks, and each new function would declare how it should behave when those hooks are invoked. Some of this happens already via declaring how the arguments affect type checking with "consistent type" concept, but its a bit adhoc.
Activity
weinig commentedon Apr 28, 2026
cc @tabatkins, @fantasai
I also put up a PR in the WPT repo with some tentative tests for
calc-mix(), web-platform-tests/wpt#59515.I picked the following answers for the questions above:
calc-mix(1 calc(10% * sibling-index(), 2)(here, becausesibling-index()isn't resolvable at specified value time), it is left un-normalized.calc-mix()should not be converted to its products and sums form.The thorny question remains of what to do in
<length-percentage>when all the terms have 0% weights, and the values are a mix of<length>and<percentage>. For examplecalc-mix(10% 0%, 10px 0%). In the tentative WPT tests, and the WebKit implementation, I have chosen to resolve that to match the type of the first term, so in this case it would becalc(0%). Had the terms been switched,calc-mix(10px 0%, 10% 0%), it would becalc(0px). I am not super satisfied with that decision, but I figured having something was a good starting point for discussion.[-][css-values] Clarifications on how should calc-mix() simplification should work[/-][+][css-values] Clarifications on how calc-mix() simplification should work[/+]Implement calc-mix()
tabatkins commentedon Jul 29, 2026
Simple answer: as soon as you can, matching normal calc simplification rules. calc-mix(), somewhat unique among the mix functions, is purely a convenience method for just writing out the calc() manually; instead of
calc-mix(A X%, B), you can writecalc(A * X + B * (1 - X)). Whenever you could simplify one of those components in thecalc(), you should be able to simplify it the same incalc-mix(), and when the entire expression could simplify away, the entirecalc-mix()should simplify away in an identical method tocalc().For your questions, I agree with all of your proposed answers. (Strongly for questions 1-3, weakly for question 4.)
For your thorny question of
calc-mix(10px 0%, 10% 0%), this definitely needs some specification, but I think the answer should be special-cased to acalc(0px + 0%), preserving the type exactly.css-meeting-bot commentedon Jul 29, 2026
The CSS Working Group just discussed
[css-values] Clarifications on how calc-mix() simplification should work, and agreed to the following:RESOLVED: calc-mix() simplifies like calc() as described in the issue, except the mixed-type case resolves to calc(0px + 0%)The full IRC log of that discussion
<TabAtkins> weinig: I implemented calc-mix(), but ther'es no simplification rules so I made my own up and specified them ehre<TabAtkins> q+
<Rossen> ack TabAtkins
<fantasai> TabAtkins: Proposed answers to answers Sam brought up are all correct in general
<fantasai> TabAtkins: Basic rule is that calc-mix() is a convenience function for linear interpolation
<fantasai> TabAtkins: So we should simplify the same as calc() would
<kbabbitt> +1
<fantasai> TabAtkins: Only one where I disagree with is the final example
<fantasai> TabAtkins: One of the questions he asked is, do we drop 0% cases
<TabAtkins> calc-mix(10% 0%, 10px 0%)
<fantasai> TabAtkins: Generally yes, that matches how we drop products from calc()
<fantasai> TabAtkins: But we can't drop entire thing, need to leave a value behind
<fantasai> TabAtkins: Here's an example that needs to simplify to something zero.
<fantasai> TabAtkins: His suggestion is to use first arg, and 0% vs 0px can lead to different behaviors
<TabAtkins> calc(0px + 0%)
<fantasai> TabAtkins: So my suggestion is we should use the resolved type of the calc-mix(), which is a blend of the two
<fantasai> TabAtkins: and resolve to calc(0px + 0%)
<TabAtkins> weinig: I think that's a better solution, so great
<fantasai> +1
<fantasai> PROPOSED: calc-mix() simplifies like calc() as described in the issue, except the mixed-type case resolves to calc(0px + 0%)
<TabAtkins> PROPOSED: Specify calc-mix() simplification to match clac() (and Sam's proposal in the issue), save for the "drop everything" case where we instead keep all the types around
<fantasai> RESOLVED: calc-mix() simplifies like calc() as described in the issue, except the mixed-type case resolves to calc(0px + 0%)
cdoublev commentedon Aug 7, 2026
Don't you want to partially resolve it to
calc-mix(10px 25%, 5em 75%)? If so, what should be the order of the items for eg.calc-mix(1px, 1em, 1px * sibling-count(), 1em, 1px)?Don't you want to at least omit all percentages when they are all equal (but some value is not resolveable), similarly as for
color-mix()?edit: I believe that zero-weighted values with a null percent hint can be safely removed, and that the term declared order should be preserved if values are averaged by unit, to prevent changing
0- + 0to0 + 0-.Simplify calc-mix() at parse time if possible
Simplify calc-mix() at parse time if possible
Simplify calc-mix() at parse time if possible
Simplify calc-mix() at parse time if possible