Change text color based on brightness of the covered background area?
Asked Answered
S

9

174

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?

Study answered 8/8, 2012 at 15:8 Comment(4)
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.Henryson
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.Henri
already answered here https://mcmap.net/q/144556/-javascript-color-contrasterTransfer
@Transfer Quite similar, and good input.. but it's not the exact answer to my question.Study
A
246

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>
Anther answered 8/8, 2012 at 15:38 Comment(1)
Can be shorted to the following, providing you pass it a object :::: const setContrast = rgb => (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000 > 125 ? 'black' : 'white'Steadman
I
124

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';
}
Infold answered 8/8, 2012 at 15:52 Comment(4)
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)?Study
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; }Bogbean
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 jsfiddle.net/1905occv/1Septuagesima
this one is coo, I would just adjust the yiq to >= 160, worked better for me.Adumbrate
S
25

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/

Study answered 10/5, 2017 at 15:16 Comment(1)
This is not what the OP is asking for. This inverts the colour, if you have a green background, you get orange text.Carminacarminative
D
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+')');
Danelle answered 8/8, 2012 at 15:22 Comment(8)
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.Henri
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).Danelle
@Danelle 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.Study
How about this? #2541981Danelle
What if the background colour is #808080!?Karat
@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.Danelle
What if the text does not have a background color, but its parent element?Fungi
@Fungi it's your job to do correct selector to select color and is not related to the question.Cacie
I
8

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)];

//const color = contrastColor("#123456"); //#fff
//const color = contrastColor("#ABCDEF"); //#000

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];
}


//demo
function setColors()
{

  for(let i = 0; i < 70; i++)
  {
    const bgColor = "#" + (~~(Math.random() * 16777216)).toString(16).padStart(6, 0);
    const color = contrastColor(bgColor);

    node = test.children[i] || document.createElement("span");
    node.style.backgroundColor = bgColor;
    node.style.color = color;
    node.textContent = bgColor;
    if (!node.parentNode)
      test.appendChild(node);
  }
}

setColors();
#test
{
  display: flex;
  flex-wrap: wrap;
  font-family: monospace;
}
#test > *
{
  padding: 0.3em;
}
<button onclick="setColors()">change</button>
<div id="test"></div>
Inpatient answered 12/6, 2022 at 21:10 Comment(0)
G
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>
Gazehound answered 26/4, 2020 at 19:28 Comment(0)
L
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)

Limassol answered 3/9, 2015 at 12:57 Comment(2)
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.Wisniewski
Hello Jørgen, I think the colourBrightness script may serve your purpose: github.com/jamiebrittain/colourBrightness.jsLimassol
S
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))
Steadman answered 20/10, 2017 at 13:53 Comment(0)
K
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.

Karat answered 19/8, 2012 at 22:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.