168

I am looking for a plugin or technique that changes a text's color or switches between predefined images/icons depending on the average brightness of the covered pixels of its parent's background-image or -color.

If the covered area of it's background is rather dark, make the text white or switch the icons.

Additionally, it'd be great if the script would notice if the parent has no defined background-color or -image and then continue to search for the nearest (from parent element to its parent element..).

What do you think, know about this idea? Is there something similar out there already? Examples?

Software Engineer
  • 15,457
  • 7
  • 74
  • 102
James Cazzetta
  • 3,122
  • 6
  • 32
  • 54
  • 1
    Just a thought rather than an answer. There may be a way of setting your colours using HSL then looking at the lightness value. If that value is above a certain value, apply a css rule. –  Aug 08 '12 at 15:12
  • 1
    you could conceivably parse out an element's background color into R,G,B (and optional alpha) values, working up the DOM tree if the alpha channel is set to zero. However, trying to determine the color of a background image is another matter entirely. – jackwanders Aug 08 '12 at 15:19
  • already answered here http://stackoverflow.com/questions/5650924/javascript-color-contraster – Pascal Aug 20 '12 at 02:59
  • @Pascal Quite similar, and good input.. but it's not the exact answer to my question. – James Cazzetta Mar 05 '13 at 15:19

9 Answers9

242

Interesting resources for this:

Here's the W3C algorithm (with JSFiddle demo too):

const rgb = [255, 0, 0];

// Randomly change to showcase updates
setInterval(setContrast, 1000);

function setContrast() {
  // Randomly update colours
  rgb[0] = Math.round(Math.random() * 255);
  rgb[1] = Math.round(Math.random() * 255);
  rgb[2] = Math.round(Math.random() * 255);

  // http://www.w3.org/TR/AERT#color-contrast
  const brightness = Math.round(((parseInt(rgb[0]) * 299) +
                      (parseInt(rgb[1]) * 587) +
                      (parseInt(rgb[2]) * 114)) / 1000);
  const textColour = (brightness > 125) ? 'black' : 'white';
  const backgroundColour = 'rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ')';
  $('#bg').css('color', textColour); 
  $('#bg').css('background-color', backgroundColour);
}
#bg {
  width: 200px;
  height: 50px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div id="bg">Text Example</div>
Ed The ''Pro''
  • 875
  • 10
  • 22
Alex Ball
  • 4,404
  • 2
  • 17
  • 23
119

This article on 24 ways about Calculating Color Contrast might be of interest to you. Ignore the first set of functions because they're wrong, but the YIQ formula will help you determine whether or not to use a light or dark foreground color.

Once you obtain the element's (or ancestor's) background color, you can use this function from the article to determine a suitable foreground color:

function getContrastYIQ(hexcolor){
    var r = parseInt(hexcolor.substring(1,3),16);
    var g = parseInt(hexcolor.substring(3,5),16);
    var b = parseInt(hexcolor.substring(5,7),16);
    var yiq = ((r*299)+(g*587)+(b*114))/1000;
    return (yiq >= 128) ? 'black' : 'white';
}
Community
  • 1
  • 1
cyang
  • 5,574
  • 2
  • 25
  • 34
  • Thanks, this is really helpful.. This depends on the set background-color.. But do you know how to get the average color of an image by running through each pixel (like in a loop)? – James Cazzetta Aug 17 '12 at 07:11
  • 6
    In es6 you can do this with: ```const getContrastYIQ = hc => { const [r, g, b] = [0, 2, 4].map( p => parseInt( hc.substr( p, 2 ), 16 ) ); return ((r * 299) + (g * 587) + (b * 114)) / 1000 >= 128; }``` – Centril Aug 22 '15 at 16:55
  • I took this function and expanded it a bit so that you could return two custom colors, rather than always black and white. Note that if the colors are two close together you may still get contrast issues, but this is a good alternative to returning absolute colors https://jsfiddle.net/1905occv/1/ – Hanna Jul 05 '17 at 11:27
  • 3
    this one is coo, I would just adjust the yiq to >= 160, worked better for me. – Arturo Sep 18 '18 at 20:31
24

mix-blend-mode does the trick:

header {
  overflow: hidden;
  height: 100vh;
  background: url(https://www.w3schools.com/html/pic_mountain.jpg) 50%/cover;
}

h2 {
  color: white;
  font: 900 35vmin/50vh arial;
  text-align: center;
  mix-blend-mode: difference;
  filter: drop-shadow(0.05em 0.05em orange);
}
<header>
  <h2 contentEditable role='textbox' aria-multiline='true' >Edit me here</h2>
</header>

Addition (March 2018): Following, a nice tutorial explaining all different types of modes/implementations: https://css-tricks.com/css-techniques-and-effects-for-knockout-text/

James Cazzetta
  • 3,122
  • 6
  • 32
  • 54
  • 1
    Surprised this isn't higher up -- this worked perfectly for me and is super simple to implement – ShaneOH Jan 15 '23 at 00:08
  • This is not what the OP is asking for. This inverts the colour, if you have a green background, you get orange text. – Gugalcrom123 May 27 '23 at 09:13
16

Interesting question. My immediate thought was to invert the color of the background as the text. This involves simply parsing the background and inverting its RGB value.

Something like this: http://jsfiddle.net/2VTnZ/2/

var rgb = $('#test').css('backgroundColor');
var colors = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
var brightness = 1;

var r = colors[1];
var g = colors[2];
var b = colors[3];

var ir = Math.floor((255-r)*brightness);
var ig = Math.floor((255-g)*brightness);
var ib = Math.floor((255-b)*brightness);

$('#test').css('color', 'rgb('+ir+','+ig+','+ib+')');
Shaz
  • 15,637
  • 3
  • 41
  • 59
jeremyharris
  • 7,884
  • 22
  • 31
  • You'd probably want to desaturate your 'inverted' color by averaging the inverted R,G,B values and setting them equal to each other. However, this solution is getting its base color from a string, and not from the CSS property of the element. To be reliable, the solution would have to dynamically obtain background colors, which usually returns rgb() or rgba() values, but could differ according to browser. – jackwanders Aug 08 '12 at 15:31
  • Yes. For ease of parsing, I just used a hex value. I updated the fiddle to include grabbing the element's color from the CSS. I updated the fiddle and included a sort of brightness control (I don't know anything about color math so it's probably not truly brightness). – jeremyharris Aug 08 '12 at 15:41
  • @jeremyharris This is a very helpful piece of code, but to extend the possibilities, do you know how to get the average color of an image by running through each pixel (like in a loop)? So, instead of grabbing the background-color via CSS we could gain the background-images average color. – James Cazzetta Aug 17 '12 at 07:14
  • 1
    How about this? http://stackoverflow.com/questions/2541481/get-average-color-of-image-via-javascript – jeremyharris Aug 18 '12 at 03:15
  • 3
    What if the background colour is `#808080`!? – Nathan MacInnes Aug 19 '12 at 22:16
  • 1
    @NathanMacInnes it'll still invert it, it just so happens that inverting something right in the middle of the spectrum will result in itself. This code just inverts the color, which comes with its limitations. – jeremyharris Aug 20 '12 at 00:38
  • What if the text does not have a background color, but its parent element? – reggie Jul 29 '15 at 14:02
  • @reggie it's your job to do correct selector to select color and is not related to the question. – Petroff Jul 06 '17 at 12:15
7

By combining the answers [ @alex-ball , @jeremyharris ] I found this to be the best way for me:

        $('.elzahaby-bg').each(function () {
            var rgb = $(this).css('backgroundColor');
            var colors = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);

            var r = colors[1];
            var g = colors[2];
            var b = colors[3];

            var o = Math.round(((parseInt(r) * 299) + (parseInt(g) * 587) + (parseInt(b) * 114)) /1000);

            if(o > 125) {
                $(this).css('color', 'black');
            }else{
                $(this).css('color', 'white');
            }
        });
*{
    padding: 9px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.0/jquery.min.js"></script>
<div class='elzahaby-bg' style='background-color:#000'>color is white</div>

<div class='elzahaby-bg' style='background-color:#fff'>color is black</div>
<div class='elzahaby-bg' style='background-color:yellow'>color is black</div>
<div class='elzahaby-bg' style='background-color:red'>color is white</div>
A. El-zahaby
  • 1,130
  • 11
  • 32
6

I've found the BackgroundCheck script to be very useful.

It detects the overal brightness of the background (be it a background image or a color), and applies a class to the assigned text-element (background--light or background--dark), dependent on the brightness of the background.

It can be applied to still and moving elements.

(Source)

cptstarling
  • 769
  • 6
  • 11
  • Does this work for background-colors? I've fast-read the script, and cant see it utilizing background-color to check for brightness. Only images. – Jørgen Skår Fischer Sep 06 '15 at 22:01
  • 1
    Hello Jørgen, I think the colourBrightness script may serve your purpose: https://github.com/jamiebrittain/colourBrightness.js – cptstarling Sep 07 '15 at 07:06
6

If you are using ES6, convert hex to RGB then can use this:

const hexToRgb = hex => {
    // turn hex val to RGB
    const 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
}

// calc to work out if it will match on black or white better
const setContrast = rgb =>
    (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000 > 125 ? 'black' : 'white'

const getCorrectColor = setContrast(hexToRgb(#ffffff))
rjb
  • 9,036
  • 2
  • 44
  • 49
Luke Robertson
  • 1,592
  • 16
  • 21
4

Here's my attempt:

(function ($) {
    $.fn.contrastingText = function () {
        var el = this,
            transparent;
        transparent = function (c) {
            var m = c.match(/[0-9]+/g);
            if (m !== null) {
                return !!m[3];
            }
            else return false;
        };
        while (transparent(el.css('background-color'))) {
            el = el.parent();
        }
        var parts = el.css('background-color').match(/[0-9]+/g);
        this.lightBackground = !!Math.round(
            (
                parseInt(parts[0], 10) + // red
                parseInt(parts[1], 10) + // green
                parseInt(parts[2], 10) // blue
            ) / 765 // 255 * 3, so that we avg, then normalize to 1
        );
        if (this.lightBackground) {
            this.css('color', 'black');
        } else {
            this.css('color', 'white');
        }
        return this;
    };
}(jQuery));

Then to use it:

var t = $('#my-el');
t.contrastingText();

This will straight away, make the text either black or white as appropriate. To do the icons:

if (t.lightBackground) {
    iconSuffix = 'black';
} else {
    iconSuffix = 'white';
}

Then each icon could look like 'save' + iconSuffix + '.jpg'.

Note that this won't work where any container overflows its parent (for example, if the CSS height is 0, and overflow isn't hidden). To get that working would be a lot more complex.

Tammy Tee
  • 105
  • 1
  • 10
Nathan MacInnes
  • 11,033
  • 4
  • 35
  • 50
4

In es6 contrast from a HEX 6-character color string (#123456) can be calculated with this one-liner:

const contrastColor = c=>["#000","#fff"][~~([.299,.587,.114].reduce((r,v,i)=>parseInt(c.substr(i*2+1,2),16)*v+r,0)<128)];

Here is broken down, readable version:

const contrastColor = color =>
{
  const lum = [.299 /*red*/,.587 /*green*/,.114 /*blue*/].reduce((result, value, index) => 
  {
    // with reduce() we can convert an array of numbers into a single number
    // result = previous result returned by this function
    // value = https://www.w3.org/TR/AERT/#color-contrast
    // index = current position index in the array
    // num = decimal number of Red, Green or Blue color
    const num = parseInt(color.substr(index * 2 + 1, 2), 16);
    return num * value + result;
  }, 0 /* result = 0 */);

  const isDark = lum < 128;
  const index = ~~isDark; // convert boolean into 0 or 1
  return ["#000","#fff"][index];
}

function setColors()
{

  for(let i = 0; i < 70; i++)
  {
    const bg = "#" + (~~(Math.random() * 16777216)).toString(16).padStart(6, 0),
          color = contrastColor(bg);
    node = test.children[i] || document.createElement("span");
    node.style.backgroundColor = bg;
    node.style.color = color;
    node.textContent = bg;
    if (!node.parentNode)
      test.appendChild(node);
  }
}

setColors();
#test
{
  display: flex;
  flex-wrap: wrap;
  font-family: monospace;
}
#test > *
{
  padding: 0.3em;
}
<button id="click" onclick="setColors()">change</button>
<div id="test"></div>
vanowm
  • 9,466
  • 2
  • 21
  • 37