87

Why does the final color of two stacked semi-translucent boxes depend on the order?

How could I make it so that I get the same color in both cases?

.a {
  background-color: rgba(255, 0, 0, 0.5)
}

.b {
  background-color: rgba(0, 0, 255, 0.5)
}
<span class="a"><span class="b">          Color 1</span></span>
<span class="b"><span class="a">Different Color 2</span></span>
Temani Afif
  • 180,975
  • 14
  • 166
  • 216
rmv
  • 3,105
  • 4
  • 22
  • 28
  • 7
    I don't know the answer to the question, but the same thing happens in photoshop and it's something that I've accepted for years as just being part of computer color theory. I'll look around to see if I can find any more information. – YAHsaves May 28 '18 at 23:41
  • 11
    For what its worth, the same thing happens in real life for anything that is not 100% transparent and is lit from the front. More light from the front object gets to your eye, thus its color has greater effect on the final color even if both have 50% transparency. – jpa May 29 '18 at 05:27
  • 4
    @YAHsaves: The average of 0 and 100 is 50 (step 1). The average of 50 and 150 is 100 (step 2). Compare this to: the average of 150 and 0 is 75 (step 1). The average of 75 and 100 is 87.5 (step 2). The issue is that the three numbers are not being weighted correctly. An average needs to be calculated based on all the numbers at the same time; you can't just recursively calculate the average step by step. (Note that the average calculcation is essentially a 50% transparency calculation. The calculation changes for different transparency levels, but the principle remains the same) – Flater May 29 '18 at 10:51
  • 2
    Lessons learned: with 'mix-blend-mode: multiply' I will get a color independent of the stacking order - that's what I was initially looking for! I think @Moffens answer is most useful to any other user facing the same problem. But the explanations of Temani Afif actually apply to my question and describe why HTML behaves the other way (it mimics physical light propagation through semi-transparent foils), so the green tick goes to him. – rmv May 29 '18 at 17:38

4 Answers4

108

Simply because in both cases the combination of colors is not the same due to how opacity of the top layer affect the color of the bottom layer.

For the first case, you see 50% of blue and 50% of transparent in the top layer. Through the transparent part, you see 50% of red color in the bottom layer (so we only see 25% of red in total). Same logic for the second case (50% of red and 25% of blue); thus you will see different colors because for both cases we don't have the same proportion.

To avoid this you need to have the same proportion for both your colors.

Here is an example to better illustrate and show how we can obtain same color:

In the top layer (the inner span) we have 0.25 of opacity (so we have 25% of the first color and 75% of transparent) then for the bottom layer (the outer span) we have 0.333 opacity (so we have 1/3 of 75% = 25% of the color and the remaining is transparent). We have the same proportion in both layers (25%) so we see the same color even if we reverse the order of layers.

.a {
  background-color: rgba(255, 0, 0, 0.333)
}

.b {
  background-color: rgba(0, 0, 255, 0.333)
}

.a > .b {
  background-color: rgba(0, 0, 255, 0.25)
}
.b > .a {
  background-color: rgba(255, 0, 0, 0.25)
}
<span class="a"><span class="b">          Color 1</span></span>
<span class="b"><span class="a">Different Color 2</span></span>

As a side note, the white background is also affecting the rendering of the colors. Its proportion is 50% which will make the logical result of 100% (25% + 25% + 50%).

You may also notice that it won't be possible to have same proportion for our both colors if the top layer is having an opacity bigger than 0.5 because the first one will have more than 50% and it will remain less than 50% for the second one:

.a {
  background-color: rgba(255, 0, 0, 1) /*taking 40% even with opacity:1*/
}

.b {
  background-color: rgba(0, 0, 255, 1) /*taking 40% even with opacity:1*/
}

.a > .b {
  background-color: rgba(0, 0, 255, 0.6) /* taking 60%*/
}
.b > .a {
  background-color: rgba(255, 0, 0, 0.6) /* taking 60%*/
}
<span class="a"><span class="b">          Color 1</span></span>
<span class="b"><span class="a">Different Color 2</span></span>

The common trivial case is when the top layer is having opacity:1 which make the top color with a proportion of 100%; thus it's an opaque color.


For a more accurate and precise explanation here is the formula used to calculate the color we see after the combination of both layersref:

ColorF = (ColorT*opacityT + ColorB*OpacityB*(1 - OpacityT)) / factor

ColorF is our final color. ColorT/ColorB are respectively the top and bottom colors. opacityT/opacityB are respectively the top and bottom opacities defined for each color:

The factor is defined by this formula OpacityT + OpacityB*(1 - OpacityT).

It's clear that if we switch the two layers the factor won't change (it will remain a constant) but we can clearly see that the proportion for each color will change since we don't have the same multiplier.

For our initial case, both opacities are 0.5 so we will have:

ColorF = (ColorT*0.5 + ColorB*0.5*(1 - 0.5)) / factor

Like explained above, the top color is having a proportion of 50% (0.5) and the bottom one is having a proportion of 25% (0.5*(1-0.5)) so switching the layers will also switch these proportions; thus we see a different final color.

Now if we consider the second example we will have:

ColorF = (ColorT*0.25 + ColorB*0.333*(1 - 0.25)) / factor

In this case we have 0.25 = 0.333*(1 - 0.25) so switching the two layers will have no effect; thus the color will remain the same.

We can also clearly identify the trivial cases:

  • When top layer is having opacity:0 the formula is equal to ColorF = ColorB
  • When top layer is having opacity:1 the formula is equal to ColorF = ColorT
Temani Afif
  • 180,975
  • 14
  • 166
  • 216
  • 25
    @ChrisHappy am not solving the problem ;) am explaining .. the explanation is sometimes better than the fix – Temani Afif May 28 '18 at 23:57
  • The problem still occurs even if you you take the div's out of each other, and position:absolute them to stack. – YAHsaves May 28 '18 at 23:57
  • 1
    @YAHsaves the problem will occur even if we use multiple background or any thing that make the two color above each other (using absolute position or not) ;) – Temani Afif May 28 '18 at 23:59
  • 5
    If you want to add a TLDR, it would be that they are multiplicative instead of additive. – Caramiriel May 29 '18 at 07:55
  • 2
    @Caramiriel: "multiplicative" would still doesn't explain why the operation isn't commutative. – Eric Duminil May 30 '18 at 07:54
41

You can use the css property, mix-blend-mode : multiply (limited browser support)

.a {
  background-color: rgba(255, 0, 0, 0.5);
  mix-blend-mode: multiply;
}

.b {
  background-color: rgba(0, 0, 255, 0.5);
  mix-blend-mode: multiply;
}

.c {
  position: relative;
  left: 0px;
  width: 50px;
  height: 50px;
}

.d {
  position: relative;
  left: 25px;
  top: -50px;
  width: 50px;
  height: 50px;
}
<span class="a"><span class="b">          Color 1</span></span>
<span class="b"><span class="a">Different Color 2</span></span>

<div class="c a"></div>
<div class="d b"></div>

<div class="c b"></div>
<div class="d a"></div>
ludovico
  • 1,083
  • 7
  • 21
Moffen
  • 1,435
  • 1
  • 8
  • 24
  • 6
    Just about any color blending mode from [this list](http://www.deepskycolors.com/archive/2010/04/21/formulas-for-Photoshop-blending-modes.html) which is "commutative" will do the trick. – Salman A May 29 '18 at 07:10
  • Interesting, this works with Chrome but not with Edge (as of the latest Windows 10 update). – Hong Ooi May 29 '18 at 08:39
  • @Salman A: so the default behavior in HTML-CSS is "Blend mode" = "Overlay" - and not "Multiply" as i mistakenly expected? – rmv May 29 '18 at 08:57
  • 3
    @rmv why would you expect anything other than overlay / normal? What is the expected color if you _position_ an opaque blue box in front of a red box? It will be blue instead of red or function of blue & red. – Salman A May 29 '18 at 09:07
  • @SalmanA my first naive guess was that it must be commutative (like additive color mixing), but, of course, it depends on order. It actually imitates physical light propagation as 'jpa' pointed out in a comment. – rmv May 29 '18 at 12:06
  • 1
    @Moffen Perfect answer, but could you please add an explanation for *why* this works, and why the original code didn't? – Bergi May 29 '18 at 14:26
  • 1
    @rmv: What the "normal" mixing mode most closely imitates is actually paint mixing. For example, if you take some white paint, replace half of it with red paint and then replace half of the resulting pink paint with blue paint, you'll end up with a bluish purple (25% white, 25% red, 50% blue). If you start with the same white paint but first replace half of it with blue and then half of the result with red, you'll end up with a *reddish* purple (25% white, 25% blue, 50% red) instead. – Ilmari Karonen May 29 '18 at 15:24
23

You are mixing three colors in the following order:

  • rgba(0, 0, 255, 0.5) over (rgba(255, 0, 0, 0.5) over rgba(255, 255, 255, 1))
  • rgba(255, 0, 0, 0.5) over (rgba(0, 0, 255, 0.5) over rgba(255, 255, 255, 1))

And you get different results. This is because the foreground color is blended with background color using normal blend mode1,2 which is not commutative3. And since it is not commutative, swapping foreground and background colors will produce different result.

1 Blending mode is a function that accepts a foreground and background color, applies some formula and returns the resulting color.

2 Given two colors, background and foreground, normal blend mode simply returns the foreground color.

3 An operation is commutative if changing the order of the operands does not change the result e.g. addition is commutative (1 + 2 = 2 + 1) and subtraction is not (1 - 2 ≠ 2 - 1).

The solution is to use a blending mode that is commutative: one that returns same color for same pair of colors in any order (for example the multiply blend mode, which multiplies both colors and returns the resulting color; or darken blend mode, which returns the darker color of the two).

$(function() {
  $("#mode").on("change", function() {
    var mode = $(this).val();
    $("#demo").find(".a, .b").css({
      "mix-blend-mode": mode
    });
  });
});
#demo > div {
  width: 12em;
  height: 5em;
  margin: 1em 0;
}

#demo > div > div {
  width: 12em;
  height: 4em;
  position: relative;
  top: .5em;
  left: 4em;
}

.a {
  background-color: rgba(255, 0, 0, 0.5);
}

.b {
  background-color: rgba(0, 0, 255, 0.5);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

<select id="mode">
  <optgroup label="commutative">
    <option>multiply</option>
    <option>screen</option>
    <option>darken</option>
    <option>lighten</option>
    <option>difference</option>
    <option>exclusion</option>
  </optgroup>
  <optgroup label="non-commutative">
    <option selected>normal</option>
    <option>overlay</option>
    <option>color-dodge</option>
    <option>color-burn</option>
    <option>hard-light</option>
    <option>soft-light</option>
    <option>hue</option>
    <option>saturation</option>
    <option>color</option>
    <option>luminosity</option>
  </optgroup>
</select>

<div id="demo">
  <div class="a">
    <div class="b"></div>
  </div>
  <div class="b">
    <div class="a"></div>
  </div>
</div>

For completeness, here is the formula to calculate composited color:

αs x (1 - αb) x Cs + αs x αb x B(Cb, Cs) + (1 - αs) x αb x Cb

with:

Cs: the color value of the foreground color
αs: the alpha value of the foreground color
Cb: the color value of the background color
αb: the alpha value of the background color
B: the blending function

Salman A
  • 229,425
  • 77
  • 398
  • 489
8

For explanation of what happens, see Temani Afif's answer.
As an alternative solution, you can take one span, a for instance, position it and give it a lower z-index if it's inside b. Then the stacking will always be the same: b is drawn on top of a in the first line, and a is drawn underneath b in the second.

.a {
  background-color: rgba(255, 0, 0, 0.5);
}

.b {
  background-color: rgba(0, 0, 255, 0.5);
}

.b .a {position:relative; z-index:-1;}
<span class="a"><span class="b">     Color 1</span></span>
<span class="b"><span class="a">Same Color 2</span></span>
Mr Lister
  • 42,557
  • 14
  • 95
  • 136