Test your Contrasts!

Yair Morgenstern
3 min readJan 12, 2023

TL;DR: Test contrasting color readability in 20 lines of code!

We’ve all seen that color combo somewhere that made us go “ugh, who thought that was a good idea?”, and this can be especially troubling if we allow users to choose their own colors, because they can sabotage themselves. Some platforms deal with this by limiting user colors to a known set of colors, but this of course limits customization.

We encountered this problem both in user colors and in our own colors, which lead us at first to polls like this one:

Color Tinder

That requires Actual People, though, who are limited on time, energy etc — and also means a relatively slow iterative process. It’s good for an initial color combo, but not for checking readability. And it’s definitely not suited for automated tests.

The good news is that Good Contrast IS Quantifiable, and therefore Testable, and therefore Fixable!

The Web Content Accessibility Guidelines - WCAG - has definitions for good contrast here, calculable per the contrast ratio between the relative luminance of the two colors.

That’s a lot of words, but it isn’t a lot of code!

Calculating Contrast

In Kotlin, the Relative Luminance function looks like this:

/** All defined by https://www.w3.org/TR/WCAG20/#relativeluminancedef */
fun getRelativeLuminance(color:Color):Double{
fun getRelativeChannelLuminance(channel:Float):Double =
if (channel < 0.03928) channel / 12.92
else ((channel + 0.055) / 1.055).pow(2.4)

val R = getRelativeChannelLuminance(color.r)
val G = getRelativeChannelLuminance(color.g)
val B = getRelativeChannelLuminance(color.b)

return 0.2126 * R + 0.7152 * G + 0.0722 * B
}

And the contrast ratio looks like this:

/** https://www.w3.org/TR/WCAG20/#contrast-ratiodef */
fun getContrastRatio(color1:Color, color2:Color): Double { // ratio can range from 1 to 21
val innerColorLuminance = getRelativeLuminance(color1)
val outerColorLuminance = getRelativeLuminance(color2)

return if (innerColorLuminance > outerColorLuminance) (innerColorLuminance + 0.05) / (outerColorLuminance + 0.05)
else (outerColorLuminance + 0.05) / (innerColorLuminance + 0.05)
}

THAT’S IT!

According to the WCAG, the very minimum for readable text is a 3:1 ratio, with a good ratio being 4.5:1. So with 20 lines of code you can now test for user readability!

Fixing contrast

Since we can now calculate what ‘good contrast’ and ‘bad contrast’ look like, we can also auto-fix existing colors to put them into better shape!

The way I went about this is:

  • Take the base colors
  • Find out using relative luminance which is brighter and which is darker
  • Each step, take a 5% brighter version of the bright color, and a 5% darker version of the dark color, until you reach a color combo that gives a good contrast ratio
  • Suggest those new colors to the user

In Kotlin that looks like this:

if (constrastRatio < 3) {
val innerColorLuminance = getRelativeLuminance(color1)
val outerColorLuminance = getRelativeLuminance(color2)

val lerpColor1:Color
val lerpColor2:Color

if (innerColorLuminance > outerColorLuminance) { // inner is brighter
lerpColor1 = Color.WHITE
lerpColor2 = Color.BLACK
}
else {
lerpColor1 = Color.BLACK
lerpColor2 = Color.WHITE
}

var text = "Colors do not contrast enough - combination is unreadable!"

for (i in 1..10){
val newColor1 = color1.cpy().lerp(lerpColor1, 0.05f *i)
val newColor2 = color2.cpy().lerp(lerpColor2, 0.05f *i)

if (getContrastRatio(newColor1, newColor2) > 3){
// the "*255" is because users speak in 0–255 RGB, and the library speaks in 0–1 RGB
text += "\n Suggested inner color: (${(newColor1.r*255).toInt()}, ${(newColor1.g*255).toInt()}, ${(newColor1.b*255).toInt()})"
text += "\n Suggested outer color: (${(newColor2.r*255).toInt()}, ${(newColor2.g*255).toInt()}, ${(newColor2.b*255).toInt()})"
break
}
}

Examples

Examples taken from Unciv, where I implemented the “autocorrect colors” as a feature for mods

Before (above) and after (below)
On first glance this looked to me like the algorithm was wrong, the upper is more inviting! But the more I looked at it the more I realized it made my eyes strain more

Conclusion

Color theory always seems like an abstract thing where people who understand complimentary colors are just more magical and their stuff Just Looks Better. That’s why standards like the Web Content Accessibility Guidelines are so important — they are built to be understood by the people that implement them, which means that even someone as incredibly mediocre at UI as me can easily implement them and make user’s lives easier

--

--

Yair Morgenstern

Creator of Unciv, an open-source multiplatform reimplementation of Civ V