110

Pretty straight-forward, take yellow and white:

back_color = {r:255,g:255,b:255}; //white
text_color = {r:255,g:255,b:0}; //yellow

What law of physics on God's Earth of universal constants, makes the fact that yellow text can't be read on white backgrounds but blue text can?

For the sake of my customizable widget I tried all possible color models that I found conversions functions for; neither can say that green can be on white and yellow can't, based on just numerical comparisons.

I looked at Adsense (which is created by the Budda of all Internet) and guess what they did, they made presets and color cells distance calculations. I can't to do that. My users have the right to pick even the most retina-inflammatory, unaesthetic combinations, as long as the text can still be read.

kaiz.net
  • 1,984
  • 3
  • 23
  • 31
Silviu-Marian
  • 10,565
  • 6
  • 50
  • 72
  • So your users can choose any two colors so long as they are readably contrasting? – DanRedux Mar 16 '12 at 07:13
  • I find this interesting from a technical point of view, but more practically if your users have the "right" to pick any colours why do you even care if it can be read? Isn't it up to them to get it right? – nnnnnn Mar 16 '12 at 07:19
  • @nnnnnn I don't really care what colors they pick, they can mix whatever they want, but I care about how readable the (c) 2012 Company Inc. is. – Silviu-Marian Mar 16 '12 at 07:42
  • I set up a jsfiddle to see for myself how accurate the answers were, and they do see to be able to predict the readability quite well: http://jsfiddle.net/UVUZC/ – mowwwalker Mar 16 '12 at 08:18
  • 1
    In case anyone missed ShaoKahn's response, [this was the link](http://snook.ca/technical/colour_contrast/colour.html). – Silviu-Marian Mar 16 '12 at 11:48
  • I found *retina-inflammatory* to be an exquisite descriptor. Touche... – IAbstract May 25 '18 at 18:11
  • The title is a bit misleasing because you use the term "contrast ratio" which is a display property. I know you want the difference in contrast between two colors. – jaques-sam Jul 02 '20 at 12:32
  • You are right. However people make the same mistake I made calling it that way, and get here, so it's a mistake worth keeping. – Silviu-Marian Jul 02 '20 at 13:18
  • If you don't want to calculate this yourself, you can ask the API of [WebAIM](https://webaim.org/resources/contrastchecker/): You send two colors, and receive the contrast ratio and pass/fail for different levels of accessibility. – lars k. Dec 30 '21 at 11:42

7 Answers7

150

According to Wikipedia, when converting to grayscale representation of luminance, "one must obtain the values of its red, green, and blue" and mix them in next proportion: R:30% G:59% B:11%

Therefore white will have 100% luminance and yellow will have 89%. At the same time, green has as small as 59%. 11% is almost four times lower than 41% difference!

And even lime (#00ff00) is not good for reading large amounts of texts.

IMHO for good contrast colors' brightness should differ at least for 50%. And this brightness should be measured as converted to grayscale.

upd: Recently found a comprehensive tool for that on the web which in order uses formula from w3 document Threshold values could be taken from #1.4 Here is an implementation for this more advanced thing.

const RED = 0.2126;
const GREEN = 0.7152;
const BLUE = 0.0722;

const GAMMA = 2.4;

function luminance(r, g, b) {
  var a = [r, g, b].map((v) => {
    v /= 255;
    return v <= 0.03928
      ? v / 12.92
      : Math.pow((v + 0.055) / 1.055, GAMMA);
  });
  return a[0] * RED + a[1] * GREEN + a[2] * BLUE;
}

function contrast(rgb1, rgb2) {
  var lum1 = luminance(...rgb1);
  var lum2 = luminance(...rgb2);
  var brightest = Math.max(lum1, lum2);
  var darkest = Math.min(lum1, lum2);
  return (brightest + 0.05) / (darkest + 0.05);
}

console.log(contrast([255, 255, 255], [255, 255, 0])); // 1.074 for yellow
console.log(contrast([255, 255, 255], [0, 0, 255])); // 8.592 for blue

// note: minimal recommended contrast ratio is 4.5, or 3 for larger font-sizes

For more information, check the WCAG 2.0 documentation on how to compute this value.

kirilloid
  • 14,011
  • 6
  • 38
  • 52
  • 4
    +1 That's true. In real life - Converting to grayscale to check designs readability is a must-do (specially for logos). – Roko C. Buljan Mar 16 '12 at 07:29
  • I'll pick this answer as solution because it is the most comprehensive and I can't pick two solutions; but I would've chosen HondaSuzukiShaolinShaorma's answer because it provides ready-to-use code. – Silviu-Marian Mar 16 '12 at 07:38
  • Yes, I like his answer too =) – kirilloid Mar 16 '12 at 07:40
  • 14
    While `contrast([255, 255, 255], [0, 0, 255])` returns 8.592, the same numbers reversed `contrast([0, 0, 255], [255, 255, 255])` returns 0.116 – i.e., 1/8.592. To get a contrast ratio between 1 and 21, you will need to divide 1 by the output if the output is < 1. `function contrast(rgb1, rgb2) { var result = (luminanace(rgb1[0], rgb1[1], rgb1[2]) + 0.05) / (luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05); if (result < 1) result = 1/result; return result; }` – mattsoave Nov 01 '17 at 19:09
  • BTW to address inversed numbers, one can write something like `Math.abs(Math.log(contrast)) >= 1.5` and `Math.abs(Math.log(contrast)) >= 2.15`, or just a plain if/else – kirilloid Nov 06 '18 at 13:44
  • 3
    Here is a little mistake: `return (luminanace(rgb1[0], rgb1[1], rgb1[2]) + 0.05) / (luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05); `. You have to divide the larger value with the smaller one. Better use this: `l1=luminanace(rgb1[0], rgb1[1], rgb1[2]) + 0.05; l2=luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05; return (Math.max(l1,l2) / Math.min(l1,l2));` – zuluk Nov 07 '18 at 10:09
  • 2
    @zuluk, you're totally right. Edited. https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-procedure – Bryan Rayner Feb 14 '20 at 19:41
  • see the errata notice regarding the usage of 0.03928. You need to use 0.04045 when paired with the usage of 12.92. as the correct standard. https://www.w3.org/WAI/GL/wiki/Relative_luminance – daredevil1234 Jun 09 '23 at 17:14
34

There are various ways for calculating contrast, but a common way is this formula:

brightness = (299*R + 587*G + 114*B) / 1000

You do this for both colors, and then you take the difference. This obviously gives a much greater contrast for blue on white than yellow on white.

orlp
  • 112,504
  • 36
  • 218
  • 315
  • 4
    just for reference apparently this is from w3c https://trendct.org/2016/01/22/how-to-choose-a-label-color-to-contrast-with-background/ https://www.w3.org/TR/AERT#color-contrast – Cody Moniz Oct 12 '18 at 06:54
  • 8
    Just FYI, the coefficients shown above (299*R + 587*G + 114*B) are related to NTSC, and are obsolete/incorrect for web colors, which are sRGB. The proper sRGB coefficients are 0.2126*R + 0.7152*G + 0.0722*B (Note that for sRGB the R G B components must be linearized first). – Myndex Apr 07 '19 at 03:55
5

const getLuminanace = (values) => {
  const rgb = values.map((v) => {
    const val = v / 255;
    return val <= 0.03928 ? val / 12.92 : ((val + 0.055) / 1.055) ** 2.4;
  });
  return Number((0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]).toFixed(3));
};

const getContrastRatio = (colorA, colorB) => {
  const lumA = getLuminanace(colorA);
  const lumB = getLuminanace(colorB);

  return (Math.max(lumA, lumB) + 0.05) / (Math.min(lumA, lumB) + 0.05);
};

// usage:
const back_color = [255,255,255]; //white
const text_color = [255,255,0]; //yellow

getContrastRatio(back_color, text_color); // 1.0736196319018405

Artem Bochkarev
  • 1,242
  • 13
  • 23
2

Recently I came across the answer on this page, and I used the code make a script for Adobe Illustrator to calculate the contrast ratio's.

Here you can see the result: http://screencast.com/t/utT481Ut

Some of the shorthand notations of the script above where confusing for me and where not working in Adobe extend script. Therefore I thought would be nice to share my improvement/interpretation of the code that kirilloid shared.

function luminance(r, g, b) {
    var colorArray = [r, g, b];
    var colorFactor;
    var i;
    for (i = 0; i < colorArray.length; i++) {
        colorFactor = colorArray[i] / 255;
        if (colorFactor <= 0.03928) {
            colorFactor = colorFactor / 12.92;
        } else {
            colorFactor = Math.pow(((colorFactor + 0.055) / 1.055), 2.4);
        }
        colorArray[i] = colorFactor;
    }
    return (colorArray[0] * 0.2126 + colorArray[1] * 0.7152 + colorArray[2] * 0.0722) + 0.05;
}

And of course you need to call this function

within a for loop I get all the colors from my illustrator object

//just a snippet here to demonstrate the notation
var selection = app.activeDocument.selection;
for (i = 0; i < selection.length; i++) {
   red[i] = selection[i].fillColor.red;
   //I left out the rest,because it would become to long
}

//this can then be used to calculate the contrast ratio.
var foreGround = luminance(red[0], green[0], blue[0]);
var background = luminance(red[1], green[1], blue[1]);
luminanceValue = foreGround / background;
luminanceValue = round(luminanceValue, 2);

//for rounding the numbers I use this function:
function round(number, decimals) {
   return +(Math.round(number + "e+" + decimals) + "e-" + decimals);
}

More information about contrast ratio: http://webaim.org/resources/contrastchecker/

Pascal Goldbach
  • 977
  • 1
  • 15
  • 34
2

Based on the kirilloid answer:

Angular Service that will calculate contrast and luminescence by passing in the hex value:

.service('ColorContrast', [function() {
var self = this;

/**
 * Return iluminance value (base for getting the contrast)
 */
self.calculateIlluminance = function(hexColor) {
    return calculateIluminance(hexColor);
};

/**
 * Calculate contrast value to white
 */
self.contrastToWhite = function(hexColor){
    var whiteIlluminance = 1;
    var illuminance = calculateIlluminance(hexColor);
    return whiteIlluminance / illuminance;
};

/**
* Bool if there is enough contrast to white
*/
self.isContrastOkToWhite = function(hexColor){
    return self.contrastToWhite(hexColor) > 4.5;
};

/**
 * Convert HEX color to RGB
 */
var hex2Rgb = function(hex) {
    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
    } : null;
};

/**
 * Calculate iluminance
 */
var calculateIlluminance = function(hexColor) {
    var rgbColor = hex2Rgb(hexColor);
    var r = rgbColor.r, g = rgbColor.g, b = rgbColor.b;
    var a = [r, g, b].map(function(v) {
        v /= 255;
        return (v <= 0.03928) ?
            v / 12.92 :
            Math.pow(((v + 0.055) / 1.055), 2.4);
    });
    return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
};

}]);
Tom
  • 679
  • 1
  • 12
  • 29
0
module.exports = function colorcontrast (hex) {
    var color = {};

    color.contrast = function(rgb) {
        // check if we are receiving an element or element background-color
        if (rgb instanceof jQuery) {
            // get element background-color
            rgb = rgb.css('background-color');
        } else if (typeof rgb !== 'string') {
            return;
        }

        // Strip everything except the integers eg. "rgb(" and ")" and " "
        rgb = rgb.split(/\(([^)]+)\)/)[1].replace(/ /g, '');

        // map RGB values to variables
        var r = parseInt(rgb.split(',')[0], 10),
            g = parseInt(rgb.split(',')[1], 10),
            b = parseInt(rgb.split(',')[2], 10),
            a;

        // if RGBA, map alpha to variable (not currently in use)
        if (rgb.split(',')[3] !== null) {
            a = parseInt(rgb.split(',')[3], 10);
        }

        // calculate contrast of color (standard grayscale algorithmic formula)
        var contrast = (Math.round(r * 299) + Math.round(g * 587) + Math.round(b * 114)) / 1000;

        return (contrast >= 128) ? 'black' : 'white';
    };

    // Return public interface
    return color;

};
davidcondrey
  • 34,416
  • 17
  • 114
  • 136
  • This looks good, but why does your `colorcontrast` method accept `hex` argument, but that argument is not used? – ctlockey Mar 09 '18 at 15:02
0

For calculate the contrast programmatically, you can use the NPM's color package; if you just want a tool for compare colors then you can use a Contrast Ratio Calculator

Jose
  • 1