HTML colors: from background to text colors
The technique presented in this post works for background and text/foreground colors. But let’s only focus on a computation of a foreground/text color from a background color (the opposite being easy to deduce inverting foreground and background in this post).
The issue is: if you have a dynamic background color you can’t just use black or white as foreground color. Otherwise, if the background color is dark, and so is your static foreground color, it will not be readable.
An easy solution is to compute the opposed color and then associate the foreground color dynamically from the background color.
Easy, easy...how do I get the opposed color? There are several ways but all don’t work for human eyes. Typically, the plain mathematical opposed color is not the best we can do.
Reminder: Hexa to RGB colors
Before going further, we need to use RGB colors (red, green, blue and each of these components are between 0 and 255, 0 being black and 255 the lighter version of the color) for the technique I’ll explain, but in the web context, we often manipulate hexa colors (#012345).
To convert an hexa color into RGB, you simply need to extract each segment of the hexa color in base 16. Here is a sample code doing it in typescript:
const HEX_TO_RGB = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i;
toRGB(hex) {
const extract = HEX_TO_RGB.exec(hex);
return {r: parseInt(extract[1], 16), g: parseInt(extract[2], 16), b: parseInt(extract[3], 16)};
}
From an hexa color passed to this function you will get an object with r, g, b attributes.
If you need the opposite version - converting an RGB color into the hexa version - you simply reverse the algorithm:
toHex(rgb): string {
return '#' + [rgb.r, rgb.g, rgb.b].map(c => {
const hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
}).join('');
}
Reversing the color
Now, given the RGB value, we will compute the opposed color (max - current component):
const MAX_RGB = 255;
reverse(hex) {
const c = this.toRGB(color);
return this.toHex({r: MAX_RGB - c.r, g: MAX_RGB - c.g, b: MAX_RGB - c.b});
}
Of course you can use this to compute the text color but the pitfall of such an algorithm is that you will get a lot of colors for the text, making the readability bad. That’s why an alternative is to limit colors to a dark and light for the foreground.
The luminance to the rescue
If we want to limit our colors we need to split an axis in N colors. Commonly, we will split a linear axis in 2, so that we have a dark and light color. But you will see that it is easy to extend to 3, 4 or more colors if you want a finer split.
The idea is to project RGB colors on a single axis. The luminance one gives you good results. This value is basically computed, giving a weight to each color component (R, G, B).
Strictly speaking we should use:
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
But often we use this version which is closer to human eyes perception:
const luminance = 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;
As you probably noticed the weights sum equals 1, so you just need to compare it to the maximum value of each component i.e. 255.
In other words, to know if a color is light or dark, you compare it to 255/2 :
const MAX_RGB = 255;
isLight(color) {
try {
const c = this.toRGB(color);
const lum = 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;
return lum > (MAX_RGB / 2.);
} catch (e) { // color not parseable
return false;
}
}
Tip: “2.” is the number of colors you want to support. For isLight() we just have a light and dark colors so we split in two. But if you want to use black, grey and white you would replace it by 3 and associate black to [0;255/3], grey to ]255/3;2*255/3] and white to ]2*255/3;255].
Conclusion
There are more advanced versions of these algorithms and particularly a non linear version of the luminance. But this would require more CPU, and will slow down the computation for a poor need. So the mentioned approximations are generally good enough for GUI needs.
Feel free to play with the formulas depending on your needs and layout.
There are several use cases for this solution. The obvious one I mentioned is to make contrast between foreground and background color. However, there are lots of flavors like computing the color of a border, making contrast between a title and text, and so on.
These algorithms can become more complicated taking into account more than 1 colors like in this post. But the overall idea to keep the computation simple and fast is to ensure that you can compare colors on a linear axis.
From the same author:
In the same category:

