Color of stacked semi-transparent boxes depends on order?

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


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>

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

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>