Just to distill Martin Sojka's excellent answer into something simple to apply, here's how to decide whether black or white text would have higher contrast on a given background color (R, G, B)
in the sRGB color space:
const float gamma = 2.2;
float L = 0.2126 * pow( R, gamma )
+ 0.7152 * pow( G, gamma )
+ 0.0722 * pow( B, gamma );
boolean use_black = ( L > pow( 0.5, gamma ) );
This assumes that R
, G
and B
are floating-point numbers ranging from 0.0 to 1.0. If what you have is, say, integers from 0 to 255, convert them to floats and divide them by 255.
(I would not suggest using colored text, both because the human eye has much poorer spatial resolution for color than for luminance, and also because combinations of highly saturated complementary colors tend to be irritating to look at.)
Note that, if L
is close to 0.52.2 ≈ 0.2, small changes in the background color could cause the most contrasting text color to flip from black to white or vice versa. To avoid this happening too frequently, you could save the previous text color and only change it if L
moves too far from the threshold:
if ( L > 0.3 ) {
use_black = true;
} else if ( L < 0.1 ) {
use_black = false;
} else {
// keep previous text color
}
Ps. If you wanted an even quicker approximation, you could round the exponent gamma
down to 2.0, allowing you to replace the pow()
with a simple multiplication:
float L = 0.2126 * R*R + 0.7152 * G*G + 0.0722 * B*B;
This approximate formula is still within ±0.05 of the correct luminance calculated with the official piecewise formula given by Martin (or with the gamma = 2.2
approximation used above, which itself is within ±0.01 of the official formula), and so more than close enough for this purpose. Besides, you can mostly compensate for the error simply by adjusting the threshold slightly.
Finally, note that, if the background has a very busy texture, it's possible that no single color may stand out from it very effectively. In such cases, you may want to consider using combinations of colors, such as black text with a white outline.
Ps. Earlier versions of this answer suggested using the uncorrected threshold L > 0.5
. However, this suggestion ignores the fact that the gamma correction used in the RGB color space approximates the perceptual non-linearity of the human eye, so that the shade of gray that looks about halfway between black and white is indeed close to RGB(0.5, 0.5, 0.5). Thus, to get an accurate measure of perceptual luminance, we really need to apply a final inverse gamma correction to the linear luminance L
— or, equivalently, just apply gamma to the thresholds as well.
This perceptual non-linearity also explains why just linearly mixing the uncorrected RGB components into a luma value, and basing the decision on that, also works reasonably well:
float luma = 0.2126 * R + 0.7152 * G + 0.0722 * B;
boolean use_black = ( luma > 0.5 );
This formula does tend to overestimate the brightness of highly saturated colors; for example, for pure green it gives luma = 0.7152
, whereas properly applying gamma correction would give pow(L, gamma) ≈ 0.478
. Fortunately, for saturated colors, either black or white will provide a decent contrast anyway.