Suppose my web app is given any random color, say purple, to use as a base accent color.
Black – or nearly black – text has a bad contrast against this purple background:
But if you make background a very very light shade of purple, it will be ok for reading and still retain some purpleness to be consistent with the base purple color:
How to automatically calculate that new color for any given base color (in JavaScript / SCSS)?
2
Answers
Explanation
This is largely dependent on a property of colour called luminance (one could conflate it with ‘brightness’), which is considered either a photometric or psychometric depending on what the colour space is.
RGB has no inherent property for luminance, rather it’s a byproduct of what your eyes do when it sees alternations in red green and blue channels. Luminance in an RGB colour model is held within the sum of its parts, so
255, 255, 0
despite not being white it’s geographically ‘close’ because two of three channels are emitting fully – you could say this has2/3
luminance over pure white (255, 255, 255
) which would be1
– fully luminescent.Text, as it has colour, also has a property of luminance. Text becomes difficult to read on a background when the luminance of the background is close to the luminance of the text – the difference perceptually between them is small because the difference in luminance (‘brightness’) would be too low to provoke your mind into perceiving them as distinct objects (text/background) rather than a cohesive and contiguous object (some square that changes colours, but recognizing the text within it would take a second look).
WebAIM has a colour contrast checker made specifically for developers to test this concept: https://webaim.org/resources/contrastchecker/. The contrast ratio being that aformentioned difference between the luminance of the background and the luminance of the text.
Therein lies the problem, how do we know what the luminance of a colour is if we use RGB and it has no luminance property for us?
The solution is to change your colour from one colour space to another which has that luminance property.
There are many options, but my favourite is YCoCg because of its simplicity. In YCoCg you have a Y channel (the luminance we want), as well as two color channels; Co (orange-blue channel), Cg (green-magenta channel)
The Wikipedia article for YCoCg has a code example which I’ll ‘lift’ for this answer.
To convert from RGB -> YCoCg
the reverse (YCoCg -> RGB) is as follows:
in both examples
tmp
is just a temporary variable you can throw away.Using this, we can convert an RGB colour into its YCoCg equivalent and then simply look at if the luminance channel (Y) is greater than or less than
0.5
. If a background colour has a Y component less than 0.5 it’s a dark colour and the text should be white, likewise if it’s greater than 0.5 it’s a bright colour and the text should then be black.Other, more perceptual colour spaces will give you better results as they separate the luminance from the hue. Some examples are HSV and HSL (common, easy, ok results), CIE-LAB and CIE-LUV (common, difficult, good results), and OK-LAB and OK-LUV (uncommon, somewhat easy, better results).
Solution (Change background colour)
In Javascript, using YCoCg, you can change the background colour to a lighter or darker shade by moving your colour from RGB -> YCoCg, changing the Y component to something higher/brighter, and then moving it back from YCoCg -> RGB. Although I had to change my initial assumption from 0.5 to 0.75 for this one to work for most cases as blues are notorious in quick luminance calculations.
Here’s an example:
Solution (Alternate text colour)
Another solution in Javascript, using YCoCg, can be achieved by alternating the text colour between white and black depending on the luminance:
Modern browsers allow you to create a color "from" another colour
For example
hsl(from purple h s 95%);
– docstakes
purple
and adjusts just the lightness keeping hue and saturation untouchedsimilar results can be gotten using lab, oklab, lch and oklch
Here’s the results using all of the above – I’ve used 95% for lightness in each case – which seems to produce a good enough result
You can however use
calc()
and maybemin()
to refine the effect furtherThe relative colours are very interesting to use in general – you can use other colour spaces as the
from
colour. So, if you already have a bunch of colours defined but you want to change them using a different colour model, you can.e.g.