Skip to content

[css-values] Clarifications on how calc-mix() simplification should work #13839

Open

Description

@weinig

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

  1. weinig commented on Apr 28, 2026

    @weinig
    ContributorAuthor

    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:

    Question 1: Should the specified value serialization include the normalized percentages (e.g. filled in the second 50%)?

    • Yes, if the normalization can be completed. In cases where it can't, like calc-mix(1 calc(10% * sibling-index(), 2) (here, because sibling-index() isn't resolvable at specified value time), it is left un-normalized.

    Question 2: Should the specified value serialization be simplified down to just calc(15px) instead?

    • Yes, if it can be fully simplified it should be.

    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)))?

    • No. calc-mix() should not be converted to its products and sums form.

    Question 4: Should 0% terms be dropped from the specified and/or computed value serializations?

    • Yes, 0% terms should be dropped as aggressively as possible, including cases where other normalization is not possible.

    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 example calc-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 be calc(0%). Had the terms been switched, calc-mix(10px 0%, 10% 0%), it would be calc(0px). I am not super satisfied with that decision, but I figured having something was a good starting point for discussion.

  2. changed the title [-][css-values] Clarifications on how should calc-mix() simplification should work[/-] [+][css-values] Clarifications on how calc-mix() simplification should work[/+] on Jun 1, 2026
  3. added a commit that references this issue on Jun 7, 2026
  4. tabatkins commented on Jul 29, 2026

    @tabatkins
    Member

    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 write calc(A * X + B * (1 - X)). Whenever you could simplify one of those components in the calc(), you should be able to simplify it the same in calc-mix(), and when the entire expression could simplify away, the entire calc-mix() should simplify away in an identical method to calc().

    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 a calc(0px + 0%), preserving the type exactly.

  5. css-meeting-bot commented on Jul 29, 2026

    @css-meeting-bot
    Member

    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%)
  6. cdoublev commented on Aug 7, 2026

    @cdoublev
    Collaborator
    test_serialization_length_percentage_computed_value_unresolved(
      'calc-mix(10px 50%, 3em 50%, 5em 50%, 7em 50%)',
      'calc-mix(10px 25%, 3em 25%, 5em 25%, 7em 25%)',
      '40px',
    );

    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()?

    If all effective percentages are known and equal to 100% / N, no percentages are serialized.

    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- + 0 to 0 + 0-.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions