Adventures in CSS Semi-Transparency Land

Avatar of Ana Tudor
Ana Tudor on

Recently, I was asked to make some tweaks to a landing page and, among the things I found in the code, there were two semitransparent overlays — both with the same RGB values for the background-color — on top of an image. Something like this:

<img src='myImage.jpg'/>
<div class='over1'></div>
<div class='over2'></div>

There was no purpose to having two of them other than the fact that having just one didn’t tint the image enough. For some reason, whoever initially coded that page thought that adding another semitransparent overlay was a better idea than increasing the opacity of the first.

So, I decided to ditch a layer and give the remaining one an opacity value that would give a visual result equivalent to the initial one, given by using two layers. Alright, but how do we get the opacity of the one layer equivalent?

If you remember my crash course in mask compositing, then you may have guessed the answer because it’s the exact same formula that we also use for the add compositing operation! Given two layers with alphas a0 and a1, the resulting alpha is:

a0 + a1 - a0⋅a1

The interactive demo below shows a comparative look at a two-layer overlay with alphas a0 and a1 (which you can control via the range inputs) versus a one layer overlay with an alpha of a0 + a1 - a0⋅a1.

See the Pen by thebabydino (@thebabydino) on CodePen.

Funny enough, they look identical if we remove the image (via the checkbox at the bottom of the demo), but seem a bit different with the image underneath. Perhaps the difference is just my eyes playing tricks on me given the image is lighter in some parts and darker in others.

It definitely doesn’t look different if we don’t have them side by side and we just switch between the two layers of alphas a0 and a1 and the one layer of alpha a0 + a1 - a0⋅a1.

See the Pen by thebabydino (@thebabydino) on CodePen.

This can be extended to multiple layers. In this case, we compute the equivalent layer of the bottom two layers, then the equivalent layer of this result and the layer right on top of it, and so on:

Diagram. Illustrates how a bunch of semitransparent layers of various alphas are reduced to a single one. We start by taking the first two from the bottom and computing their equivalent, then we take this result and the third layer from the bottom and combine them into a single layer and so on.
Reducing multiple semitransparent layers to a single one.

Playing with this has also made me wonder about the solid background equivalent of a solid layer (c0) with a semitransparent overlay (c1 with an alpha of a) on top. In this case, the background of the one layer equivalent is computed on a per channel basis, with the resulting channels being:

ch0 + (ch1 - ch0)*a

…where ch0 is a channel (red, green or blue) of the solid bottom layer, ch1 the corresponding channel of the top semitransparent layer, and a the alpha of the same top semitransparent layer.

Putting this into Sass code, we have:

/* per channel function */
@function res-ch($ch0, $ch1, $a) {
  @return $ch0 + ($ch1 - $ch0)*$a
}

@function res-col($c0, $c1, $a) {
  $ch: 'red' 'green' 'blue'; /* channel names */
  $nc: length($ch); /* number of channels */
  $ch-list: ();

  @for $i from 0 to $nc {
    $fn: nth($ch, $i + 1);
    $ch-list: $ch-list, 
      res-ch(call($fn, $c0), call($fn, $c1), $a);
  }

  @return RGB($ch-list)
}

The interactive demo below (which lets us pick the RGB values of the two layers as well as the alpha of the top one by clicking the swatches and the alpha value, respectively) shows a comparative look at the two layer versus our computed one layer equivalent.

See the Pen by thebabydino (@thebabydino) on CodePen.

Depending on the device, operating system and browser, you may see the two panels in the demo above have identical backgrounds… or not. The formula is correct, but how different browsers on different operating systems and devices deal with the two layer case may vary.

Screenshot collage.
Expected result with panels being identical on the left vs. the slightly different result we may sometimes get between the two layer scenario (top right) and the one layer scenario (bottom right).

I asked for screenshots of a simplified test case on Twitter and, from the replies that I got, the two panels always seem to look the same on mobile browsers, regardless of whether we’re talking about Android or iOS devices as well as Firefox, regardless of the operating system. They also seem to almost always be identical on Windows, though I did receive a reply letting me know both Chrome and Chromium Edge may sometimes show the two panels differently.

When it comes to WebKit browsers on macOS and Linux, results are very much mixed, with the panels slightly different in most cases. That said, switching to a sRGB profile could make them identical. The funniest thing here is, when using a two monitor setup, dragging the window from one monitor to the other can make the difference whether the two panels appear or disappear.

However, in a real use case scenario, the difference is pretty small and we’re never going to have the two panels side-by-side. Even if there’s a difference, nobody is going to know about it unless they test the page in different scenarios, which is something probably only a web developer would do anyway. And it’s not like we don’t also have differences between how the same plain old solid backgrounds look on different devices, operating systems and browsers. For example, #ffa800, which gets used a lot here on CSS-Tricks, doesn’t look the same on my Ubuntu and Windows laptops. The same can be said about the way people’s eyes may perceive things differently.