Skip to content

Notes on Handling Opacity When Determining Color Contrast Ratio with Online Tools ​

This document addresses how to calculate the color contrast (CC) ratio when elements have an opacity less than 1. During the development of the detector, we identified a potential improvement to this process for future audits. The document covers some technical topics about colors, which we attempt to exemplify and explain in a way that is understandable to any reader.

We start with some definitions that will facilitate the rest of the document.

Definitions ​

This section defines the concepts of opacity and alpha-composition.

Opacity: Defines how opaque an element is on the page. An element with opacity = 1 will be fully opaque. This means the element completely "covers" what is behind it. However, elements with opacity < 1 allow what is behind them to be seen to some extent. If the opacity is zero, the element will be fully transparent and what is behind it will be perfectly visible. Opacity values range from 0 to 1, where 0 is fully transparent and 1 is fully opaque. From now on, we will use the term non-opaque element to refer to elements with opacity < 1.

Opacity can be included in the background or foreground color of an element. For example, the color rgba(255, 0, 0, 0.5) is a red with opacity = 0.5. This color format is called rgba, as it includes the classic red, green, and blue values to define a color, but also adds the alpha value, which represents opacity. The term alpha is commonly used to refer to opacity, as we will see later.

Opacity can also be defined with the CSS property opacity.

Alpha-composition: As we saw in the previous definition, a non-opaque element allows the elements behind it to be seen to a greater or lesser extent. For example, if an element has a red background with opacity less than 1, say rgba(255, 0, 0, 0.5), and behind it there is a fully white element, a blend occurs between the non-opaque red and the white behind it. The user ends up perceiving a color closer to pink due to the blending of red and white. If the opacity increases toward 1, the user will perceive a more reddish color; if opacity decreases toward zero, the user will perceive a color closer to white. But what color exactly does the user perceive — what is its RGB or hexadecimal code?

In this case, we are blending a red with alpha = 0.5, i.e., rgba(255, 0, 0, 0.5) with a white rgba(255, 255, 255, 1). With this data, a formula can be applied to obtain the exact resulting color. This is what is known as alpha-composition. Below we describe the alpha-composition process for these two colors, but it is entirely optional for understanding the rest of the document.

Alpha composition in action (optional): First, we normalize the color values to be between 0 and 1 by dividing each channel (red, green, blue) by 255. The colors then become rgba(1, 0, 0, 0.5) and rgba(1, 1, 1, 1). Next, we calculate the resulting alpha of the combination using the formula: $\alpha_r = \alpha_f + \alpha_b(1 - \alpha_f)$ where $\alpha_r$ is the resulting alpha, $\alpha_f$ is the foreground alpha (0.5), and $\alpha_b$ is the background alpha (1).

Using our example values: $\alpha_r = 0.5 + 1(1 - 0.5) = 1$. This means our resulting alpha will be 1. Now we modify each color channel as follows: $C_r = \frac{C_f\alpha_f + C_b\alpha_b(1 - \alpha_f)}{\alpha_r}$. Where $C_r$, $C_f$, and $C_b$ are the resulting, foreground, and background channels, respectively. Applying this to each channel (red, green, and blue), we obtain the final color:

Red: $C_r = \frac{1 \times 0.5 + 1 \times 1\times 0.5}{1} = 1$

Green: $C_r = \frac{0 \times 0.5 + 1 \times 1 \times 0.5}{1} = 0.5$

Blue: $C_r = \frac{0 \times 0.5 + 1 \times 1 \times 0.5}{1} = 0.5$

We then multiply each channel by 255 and round it, giving us rgba(255, 128, 128, 1) as the result. As can be seen, this is a pinkish color.

Next, we explain how colors are calculated for the CC ratio in the detector.

Handling Opacity for the CC Ratio in the Detector ​

To calculate the CC ratio in any scenario, the background and foreground colors are needed. In the detector, the distinction between background and foreground is critically important because when elements are non-opaque, both colors are calculated differently. As we will see, this is one of the key differences between the CC ratio calculated in the detector and the one calculated by online tools.

Consider the following example: the text Foreground is fully black and opaque (rgba(0, 0, 0, 1)). The blue background is rgba(165, 216, 255, 0.5) or #A5D8FF with opacity 0.5, and the Container element that holds everything is fully white (rgba(255, 255, 255, 1)).

To calculate the foreground color in the detector, we perform alpha-composition between the foreground color and the background of the same element. That is, we blend the fully opaque black of the Foreground text with the non-opaque background. Since the foreground color is fully opaque, no blending occurs and the resulting color is black (rgba(0, 0, 0, 1)).

To calculate the non-opaque background color, we perform alpha-composition between the element's background and its container element, resulting in a lighter color than the initial blue. Applying the formula from the previous section, we get rgba(210, 236, 255, 1) or #D2ECFF. Using Paint's color picker, we get a very similar color with a small difference due to rounding and rendering.

If we then use these two colors (rgba(0, 0, 0, 1) and rgba(210, 236, 255, 1)) in a CC ratio tool, the resulting ratio is comfortably above the threshold.

When opacity is set on the element itself ​

The rule above — foreground blended over its own background, background blended over its container — describes an alpha carried inside a color value (rgba(...)). The CSS opacity property behaves differently, and the detector handles it as a separate case.

An opacity below 1 does not fade the text and the surface independently. It flattens the whole element — its background and its glyphs — into a single layer, and only then composites that layer over whatever is behind it. Blending the text through the element's own background, as in the base case, would fold that background into the glyphs a second time and report a color the screen never shows.

The detector recognizes this situation by its signature: both the foreground and the background end up carrying a sub-1 alpha, and the two alphas are practically identical (within 0.05). When that happens it:

  1. takes a small screenshot of the element and eyedroppers the visible background pixel — that pixel already is the composite, so no formula is needed;
  2. derives the visible text color from that same backdrop and that same alpha, instead of blending it through the element's background.

Example: a green button with background: rgb(38, 180, 74); color: #fff and :hover { opacity: .85 }. Blending the white text through the green would give a greenish #def0e3; the screen actually paints a light grey #dddede, and that is the value the detector reports.

What the report shows ​

Because the numbers and the colors in a finding have to agree with each other, the report never shows a raw 8-digit value on its own:

  • The background is shown already composited. A translucent background is displayed as the color it resolves to over its backdrop, which is the same color the ratio was measured against.
  • The foreground is flattened the same way. A translucent text color is shown as the visible blended color, so that its color name (for example "grey" rather than "black") matches what a reader sees.
  • The authored value is not hidden. When a color was declared with an alpha channel, the report writes it as approximate #RRGGBB (real value: #RRGGBBAA), so a developer can still find the declaration in the style sheet.
  • A color read from the screen is labelled as such. When the surface had to be eyedroppered rather than derived from CSS, the report describes it as sampled <color> background, approx (#hex) — the same approx marker used for gradients and background images.

Fix suggestions are calculated from these visible, flattened colors too, so a suggested value can be applied and re-measured directly.

Next, we explain how the CC ratio tool is currently used in audits and see that the results differ from the above process.

Direct CC Ratio Calculation in Audits ​

Using the same example, we have a black foreground and a non-opaque background. However, most online tools used in audits do not allow entering a non-opaque background, and even if they did, the tool has no information about the color behind that background. Therefore, what would be done in these cases is to use the background color as foreground and vice versa. When colors are fully opaque, swapping background and foreground does not affect the CC ratio result, but when non-opaque colors are involved, there is a difference.

In the example above, an auditor would directly use the background color #A5D8FF with alpha 0.5 as the foreground and black as the background.

The resulting CC ratio is completely different. A CC violation is even reported when in reality the contrast is more than sufficient if we take into account what the user actually perceives. Analyzing the examples given by the tool, we can understand what is happening.

The tool assumes that our background is literally black and our foreground is a non-opaque bluish color. The alpha-composition is therefore performed between the blue and the black. So the luminosity and ultimately the CC ratio are very different from the real scenario.

Conclusions ​

The difference in methodology when calculating CC ratios between the detector and manual audits can result in large discrepancies in the final result and even differences in the final issue reports.

This document was written by a developer who is not deeply familiar with the details of audits and how the a11y team calculates color contrast. It is based on error reports from the detector's QA process. However, if it is confirmed that the described CC ratio calculation methodology is indeed the one used in audits, it is recommended to have conversations to determine whether the detector should adapt to this methodology or whether the methodology should adapt to the detector's approach.

In the meantime, the reporting rules described above are what keep the two readings reconcilable: the finding shows the composited color the detector measured and the authored value with its alpha channel, so a reviewer can reproduce either calculation without guessing which one produced the reported ratio.

Appendix ​

Conversation with ChatGPT on the subject (the alliBot's answer to the same question was very similar): https://chatgpt.com/share/685146f0-fbf4-8001-b3ef-1117e8b61922