Tag: Colors

Adjust text color to be readable on light and dark backgrounds of user interfaces

Most modern user interfaces are supporting different color schemes for day and night: the so called light and dark modes. Selecting a text color for each of those modes is not a big deal and it’s the way to go when designing the user interface.

In some cases, the text color is driven by the displayed contents. In the example below, the tint color is matched to the color of the drink. The global tint color of this app is totally different, but this color adjustment gives a very nice effect. But as you might already see, there is a small problem when it comes to very light or very dark colors: each color either has a good readability on light or dark backgrounds. Some colors might fit to both, but that’s not always the case. In the example below, the light yellow is still visible, but when it comes to small icons or small text, the details are lost.

To overcome this issue, a simple solution is to select two colors for each recipe so that each mode has a different one. That’s fine, but it might totally change the effect of this colored pages.

Can we calculate a suitable color?

Some time ago, there was an article about Black or white text on a colour background? In this one, I described different algorithms to calculate the best text color (black or white) for a colored background. But now, we need the opposite: a colored text that has a good readability on white (light) or black (dark) backgrounds.

When we look at HSL and HSV/HSB color models, we already have a value for ‘lightness’ or ‘brightness’. The idea is to find a color that matches a given hue and saturation and that has a brightness which is readable on light and dark background. For this, we can use different algorithms. Very good results could be achieved with a ‘Weighted W3C Formula‘. This formula take into consideration that the human eye perceives some of the primary colors darker than others.

f'(x) = r ? 0.299 + g ? 0.587 + b ? 0.11

Each color that is located at the border between the black and white overlay is suitable for light and dark backgrounds.

Step 1: convert the given color to HSV/HSB

Step 2: keep hue and saturation constant and adjust the brightness (make the color lighter or darker)

Step 3: convert the HSV/HSB value back to the required color format

Implementation in PHP

A simple calculation for a given RGB color is shown below. The classes used in this snippet are available on GitHub. The code checks the initial brightness of the color and lightens or darkens the values until the ‘border’ calculated by the ‘Weighted W3C Formula’ is reached. This is the case for the value 127, the total range of the brightness is 0 to 255.

$hsv = Convert::rgb2hsv($rgb);

$step = 0.01;
$brightness = Calculate::weightedW3C($rgb);
if ($brightness < 127) {
    while ($brightness < 127 && $hsv[2] >= 0 && $hsv[2] <= 1) {
        $hsv[2] += $step;
        $brightness = Calculate::weightedW3C(Convert::hsv2rgb($hsv));
    }
} else {
    while ($brightness > 127 && $hsv[2] >= 0 && $hsv[2] <= 1) {
        $hsv[2] -= $step;
        $brightness = Calculate::weightedW3C(Convert::hsv2rgb($hsv));
    }
}

return Convert::hsv2rgb($hsv);

Some examples

But how does this result look for different colors? Let’s start with some dark colors. Those are fine for a light background, but they become unreadable on a dark one. The top colors show the input color (before) and the color below shows the output of the calculation above (after).

Color #632300 adjusted to be readable on light and dark background
Color #454545 adjusted to be readable on light and dark background

And now let’s look at some light colors which are fine for dark backgrounds, but they are totally unreadable on light backgrounds.

Color #73FEFF adjusted to be readable on light and dark background
Color #F0C&96 adjusted to be readable on light and dark background

The last color is similar to the example at the beginning and as you can see, the optimized color has a much better readability. This could be achieved for both light and dark colors. The code example shown above is written in PHP. An adoption should be easily possible for any other coding or scripting language

The algorithm mentioned in this post is also available on GitHub https://github.com/mixable/color-utils. This package is usable with composer:

composer require mixable/color-utils

The optimized color can be calculated with:

use Mixable\Color\Calculate;
// ...

$hex = '#ffcc00';
$optimizedColor = Calculate::readableColorForLightAndDarkBackground($hex);

Black or white text on a colour background?

How to calculate programmatically whether black text or white text is more readable on a colour background?

Here are some algorithms to calculate the brightness of a color. The example code is written in PHP, but you should be able to adapt the code examples to any other programming language. All of the algorithmns are applied on the following color map to have a first impression how they perform.

Arithmetic Mean

f'(x)= \frac{a+b+c}{3}

In Code, this looks like:

function arithmeticMean($hex)
{
    $rgb = ColorUtils::hex2rgb($hex);
    return ($rgb[0] + $rgb[1] + $rgb[2]) / 3;
}

This results in an output for black/white text like:

This is a very simple and effective algorithm. Unfortunately it fails between the green and turquoise region:

The quick brown fox jumps over the lazy dog

Geometric Mean

f'(x)=\sqrt[n]{rgb}

In Code, this looks like:

function geometricMean($hex)
{
    var $rgb = ColorUtils::hex2rgb($hex);
    return pow($rgb[0] * $rgb[1] * $rgb[2], 1/3);
}

This results in an output for black/white text like:

Now the text is much better with the green background, but now the brightness of the yellow is wrong:

The quick brown fox jumps over the lazy dog

Quadratic Mean

f'(x)= \sqrt{\frac{r^2 +g^2+ b^2}{3}}

In Code, this looks like:

function quadraticMean($hex)
{
    var $rgb = ColorUtils::hex2rgb($hex);
    return sqrt( (pow($rgb[0], 2) + pow($rgb[1], 2) + pow($rgb[2], 2)) / 3);
}

This results in an output for black/white text like:

Works really well for all dark colours. Fails on purple.

The quick brown fox jumps over the lazy dog

HSV Value (Brightest Component)

f'(x)= max(r,g,b)

In Code, this looks like:

function valueFromHSV($hex)
{
    var $hsv = ColorUtils::hex2hsv($hex);
    return $hsv[2] * 255;
}

This results in an output for black/white text like:

Dark colours are ok, Fails when the r, g or b value is close to 0x99.

Darkest Component

f'(x)= min(r,g,b)

In Code, this looks like:

function valueFromHSV($hex)
{
    var $rgb = ColorUtils::hex2rgb($hex);
    return min($rgb[0], $rgb[1], $rgb[2]);
}

This results in an output for black/white text like:

Only the very bright colours work fine with this algorithm.

The quick brown fox jumps over the lazy dog

3D distance in RGB space

f'(x)= \sqrt{r^2 +g^2+ b^2}

In Code, this looks like:

function distanceIn3D($hex)
{
    var $rgb = ColorUtils::hex2rgb($hex);
    return sqrt( pow($rgb[0], 2) + pow($rgb[1], 2) + pow($rgb[2], 2));
}

This results in an output for black/white text like:

Worse than using the V component of the HSV representation.

The quick brown fox jumps over the lazy dog

Lightness From HSL

In Code, this looks like:

function lightnessFromHSL($hex)
{
    var $hsl = ColorUtils::hex2hsl($hex);
    return $hsl[2] * 255;
}

This results in an output for black/white text like:

The best unweighted formula. Fails on yellow around 0xeeee00.

The quick brown fox jumps over the lazy dog

Weighted W3C Formula

Natural formulas don’t take into consideration that the human eye perceives some of the primary colours darker than others.

Eg pure green(0xff0000) is perceived brighter than pure blue(0x00ff00).
According to the W3C consortium, this biased perception can be modelled with the following weights:

r *= .299
g *= .587
b *= .111
f'(x)= r\cdot0.299 +g\cdot0.587+ b\cdot0.111

In Code, this looks like:

function weightedW3C($hex)
{
    var $rgb = ColorUtils::hex2rgb($hex);
    return $rgb[0] * 0.299 + $rgb[1] * 0.587 + $rgb[2] * 0.114;
}

This results in an output for black/white text like:

Very good algorithm, works really well.
Source: http://www.w3.org/TR/AERT#color-contrast

The quick brown fox jumps over the lazy dog

Weighted Distance in 3D RGB Space

f'(x)= \sqrt{r^2\cdot0.241 +g^2\cdot0.691+ b^2\cdot0.068}

In Code, this looks like:

function weightedDistanceIn3D($hex)
{
    var $rgb = ColorUtils::hex2rgb($hex);
    return sqrt(pow($rgb[0], 2) * 0.241 + pow($rgb[1], 2) * 0.691 + pow($rgb[2], 2) * 0.068);
}

This results in an output for black/white text like:

This is an improved version of the W3C fomula.
Source: http://www.nbdtech.com/Blog/archive/2008/04/27/Calculating-the-Perceived-Brightness-of-a-Color.aspx

The quick brown fox jumps over the lazy dog

ColorUtils

The used ColorUtils class above consists of methods that are used to convert colors between different color spaces. The library is available here.

If you are interested in the source code to generate the color map examples above:


Photo by Markus Spiske on Unsplash. Image map by Wikimedia Commons.

PHP ColorUtils

A neat collection of useful methods to convert colors between different color spaces is available on GitHub (MIT License). It can convert any values between RGB, HSL, CMYK, YUV and HEX colors.

I created a ready-to-use php class out of this method collection:

https://github.com/mixable/color-utils

Usage

The methods itself are mostly self-explaining. To use the class, just include the php file into your script and you are ready to go:

<?php
require('ColorUtils.php');

The methods itself are static, they can be used like:

$hex = '#ffcc00';
$rgb = ColorUtils::hex2rgb($hex);

Which results in an array with three values (red, blue, green) in $rgb:

// [0 => 255, 1 => 204, 2 => 0]

Photo by Kelli Tungay on Unsplash