# Automatically Maximize Text Contrast On A Page

An upcoming personal project faces an interesting UI challenge in which web app users will have the opportunity to change the background color of a page. At the same time, instructions on the page must remain legible, which is impossible if the text remains a fixed color.

The most straightforward solution to this problem is to calculate the perceived “lightness” or “darkness” of the background, and then modify the color of the text so that is opposite that value.

## Calculating Luminosity

The equation to calculate the relative luminosity of a color is well known, and fairly straightforward. If each color component (red (r), green (g) and blue (b)) is provided as a value between 0 and 1, and gamma (γ) (the compensation for the non-linear light/dark perception of human vision) is 2.2, then:

luminosity = 0.2126 * rγ + 0.7152 * gγ + 0.0722 * bγ

The result of this calculation is a floating point value between 0 and 1, representing the relative luminosity of any color.

## Getting The Background Color

Our first challenge is getting the background color of the page. You might expect the following to work:

var bgColor = document.body.style.backgroundColor;

However, the result will most likely be an empty string. document.body.style.backgroundColor will only get the inline style of the element. Instead, we need the background color of the body after all styles have been resolved. That’s getComputedStyle:

var computedStyle = getComputedStyle(document.body, null);
var bgColor = computedStyle.backgroundColor;

Handily, in modern browsers this provides us with the background color in rgb format:

rgb(10,0,255)

This color format will be returned no matter how the original style might have been applied, or even if it wasn’t defined at all. However, we need each individual red, green and blue component, not the entire rgb string. To tease out those values, I’ll create a function:

function splitComponent(color) {
var rgbColors=new Object();
color = color.substring(color.indexOf('(')+1, color.indexOf(')'));
var result = color.split(',', 3);
return result ? {
r: parseFloat(result[0]/255),
g: parseFloat(result[1]/255),
b: parseFloat(result[2]/255)
} : null;
}

Very simply, the function strips away the rgb prefix and parentheses, splits the numbers by looking at the commas between them, then takes each color component and divides it by 255, to yield a floating point value between 0 and 1.

## Setting Contrast

Returning to the main code, I’ll call on this function to judge the luminosity of the background:

var luminosity =
0.2126 * Math.pow(splitComponent(bgColor).r,gamma) +
0.7152 * Math.pow(splitComponent(bgColor).g,gamma) +
0.0722 * Math.pow(splitComponent(bgColor).b, gamma);

Once I have that, I can make a judgment on how to color the text above it:

if (luminosity < 0.5) {
document.body.style.color = "#fff"
} else {
document.body.style.color = "#000"
}

## All Together Now

The complete code for the example above, starting with the HTML:

<div id="ledge">
<h1>I SHALL ALWAYS REMAIN LEDGIBLE</h1>
<label for="bgcolor">Set the background color for this element:</label>
<input type="color" value="#777777" id="ledgebg" name="ledgebg" oninput="changeBG(ledgebg.value)">
</div>

Initial CSS:

#ledge {
background-color: #332;
color: #fff;
font-family: Avenir, sans-serif;
text-align: center;
}

And JavaScript:

function splitComponent(color) {
var rgbColors=new Object();
color = color.substring(color.indexOf('(')+1, color.indexOf(')'));
var result = color.split(',', 3);
return result ? {
r: parseFloat(result[0]/255),
g: parseFloat(result[1]/255),
b: parseFloat(result[2]/255)
} : null;
}
var gamma = 2.2,
ledge = document.getElementById("ledge");
function changeBG(colorValue){
ledge.style.backgroundColor = colorValue;
var computedStyle = getComputedStyle(ledge, null);
var bgColor = computedStyle.backgroundColor;
var luminosity =
0.2126 * Math.pow(splitComponent(bgColor).r,gamma) +
0.7152 * Math.pow(splitComponent(bgColor).g,gamma) +
0.0722 * Math.pow(splitComponent(bgColor).b, gamma);
if (luminosity < 0.5) {
ledge.style.color = "#fff"
} else {
ledge.style.color = "#000"
}
}

Interestingly, the color input sets the backgroundColor of an element in hex. Rather than trying to convert that value into red, green and blue components and then into floating point values, I’ve chosen to read the getComputedStyle directly after changing the background color.

## Alternative Applications

It might be useful to build similar functionality into a Sass mixin, if only as a method of preventing “designer creep” (i.e. the tendency for designers to choose ever-lighter low-contrast colors with thinner, finer typefaces): check out the work by Mike and Ana that extends this idea.

An alternative approach would be to convert the background color into a HSL value, adding 180 to the hue component to swing the text around on the color wheel to the opposite, so-called “complementary” color. However, the combination of complementary colors is not always high contrast or aesthetically pleasing; I would tend to stick with black and white.

Obviously this technique only works with flat background colors and text, not background images; advanced techniques will need to use <canvas> or CSS overlay blend mode for text.