Skip to main content
  1. Posts/

Choosing font color based on background

Table of Contents

While working on a small project I recently ran into a situation where given a random background color I needed to decide whether white or black font color should be used to make sure a text remains readable to the end user. During my research I found a solution to this issue that I want to write down here for documentation purposes and hopefully to aid other people out there in the same situation.

Solution #

Most of the solution is taken from the Web Content Accessibility Guidelines (WCAG) released by the W3C. This standard provides recommendations to improve accessibility of the web and also includes information about the relation between the color of text and its background. Section 1.4.3 of the guideline states that an adequate light-dark contrast between text and its background is needed for good readability. To meet this requirement a contrast ratio of \(4.5:1\) should be reached.

The WCAG defines the contrast ratio between two colors with the following formula:

$$ Contrast = \frac {L_{1} + 0.05} {L_{2} + 0.05} $$

Here, \(L_{1}\) is the relative luminance of the lighter color and \(L_{2}\) the relative luminance of the darker color. Lighter color in this case means the color with the higher relative luminance value.

The relative luminance \(L\) of a given color in turn describes the “[…] relative brightness of any point in a color space” and is calculated with this formula taken from the guideline:

$$ L = 0.2126 \cdot R_{linear} + 0.7152 \cdot G_{linear} + 0.0722 \cdot B_{linear} $$

Please note that the values \(R_{linear}\), \(G_{linear}\) and \(B_{linear}\) used here are not the standard RGB values that typically define a color in the sRGB color space. Instead, these are linearized values where gamma correction took place. To calculate these values the WCAG specifies two formulae depending on the value of \(C\) which is the standard, gamma encoded \(R\), \(G\) or \(B\) value of the corresponding channel.

$$ C_{linear} = \begin{cases} \frac{C}{12.92} &\text{if } C \leq 0.04045 \\[5pt] \left(\frac{C+0.055}{1.055}\right)^{2.4} &\text{if } C > 0.04045 \end{cases} $$

Note

This formula expects the value for \(C\) to be from the normalized range \([0,1]\). Hence, if you have standard 8-bit values in the range \([0,255]\) you first need to divide them by 255 to obtain the normalized version.

As written in the short introduction in the case of my project I was only interested in using either black or white text and always choosing the one with the higher contrast in regard to the background color. Other text colors weren’t relevant. Using these limitations I could simplify the calculation and decision process.

A black text color is always the darker color during the contrast ratio calculation, as its definition of \(sRGB(0,0,0)\) means that its relative luminance is 0. This allows us to set \(L_{2} = 0\) so the formula to calculate the contrast between a background color and black text becomes the following, with \(L_{BG}\) being the relative luminance of the background:

$$ Contrast_{Black}=\frac {L_{BG} + 0.05} {0.05} $$

Similarly, white text color is always the lighter color, as its definition of \(sRGB(255,255,255)\) means that its relative luminance is 1. This allows us to set \(L_{1} = 1\) and the formula becomes:

$$ Contrast_{White}=\frac {1.05} {L_{BG} + 0.05} $$

As I always want to use the text color with the higher contrast ratio and both equations only use \(L_{BG}\) as a variable we can set them equal to one another and solve for \(L_{BG}\) to find the threshold:

$$ \begin{aligned} \frac {L_{BG} + 0.05} {0.05} &= \frac {1.05} {L_{BG} + 0.05} && \text{(Multiply by $(L_{BG} + 0.05)$)} \\[5pt] \frac {(L_{BG} + 0.05)^2} {0.05} &= 1.05 && \text{(Multiply by $0.05$)} \\[5pt] (L_{BG} + 0.05)^2 &= 0.0525 && \text{(Square root)} \\[5pt] (L_{BG} + 0.05) &\approx 0.229 && \text{(Subtract $0.05$)} \\[5pt] L_{BG} &\approx 0.179 \end{aligned} $$

This calculated approximation of \(0.179\) represents the threshold that decides which text color to use based on the luminance of the background. If \(L_{BG}\) is less than the threshold this means that using white as text color provides a greater contrast and should therefore be used. If \(L_{BG}\) is greater than the threshold black text provides a greater contrast.

Example #

To illustrate this solution I’m going to show a short example. Let’s assume I want to decide whether I should use black or white text if the background uses the color Bright Amber:

Bright Amber
Bright Amber (Hex FACA16 - RGB: 250/202/22)

This color uses the sRGB color values \(250/202/22\), which we first need to normalize to values between \(0\) and \(1\):

$$ \begin{aligned} R &= \frac{250}{255} \approx 0.9804 \\[10pt] G &= \frac{202}{255} \approx 0.7922 \\[10pt] B &= \frac{22}{255} \approx 0.0863 \end{aligned} $$

With that out of the way we can use these values to calculate \(R_{linear}\), \(G_{linear}\) and \(B_{linear}\) that are needed for the luminance:

$$ \begin{aligned} R_{linear} &= \left(\frac{0.9804+0.055}{1.055}\right)^{2.4} &\approx 0.9560 \\[10pt] G_{linear} &= \left(\frac{0.7922+0.055}{1.055}\right)^{2.4} &\approx 0.5907 \\[10pt] B_{linear} &= \frac{0.0863}{12.92} &\approx 0.0067 \end{aligned} $$

By plugging the results into the formula for \(L\) we can calculate the relative luminance of the background color Bright Amber:

$$ \begin{aligned} L_{BG} &= 0.2126 \cdot 0.9560 + 0.7152 \cdot 0.5907 + 0.0722 \cdot 0.0067 \newline &\approx 0.6262 \end{aligned} $$

Since this value is above the threshold of \(0.179\) we should use black as text color.

To verify our choice we calculate the contrast between our background and black text. In this case \(L_{1}\) equals the relative luminance of our background as it’s the lighter color and \(L_{2}\) equals the relative luminance of black text as the darker color:

$$ Contrast = \frac {L_{1} + 0.05} {L_{2} + 0.05} = \frac{0.6262 + 0.05}{0 + 0.05} = 13.524 $$

The resulting contrast ratio of \(13.52:1\) is above the goal of \(4.5:1\) from the WCAG, so our decision seems to be correct.

I hope this explanation was helpful to you out there. I’ll probably share the project where I used this feature in a future blog post. Thanks for reading and until then 👋.