177

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?

4
  • 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. Commented Aug 8, 2012 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. Commented Aug 8, 2012 at 15:19
  • already answered here stackoverflow.com/questions/5650924/javascript-color-contraster Commented Aug 20, 2012 at 2:59
  • @Pascal Quite similar, and good input.. but it's not the exact answer to my question. Commented Mar 5, 2013 at 15:19

9 Answers 9

249
+50

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>

Sign up to request clarification or add additional context in comments.

1 Comment

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'
127

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

4 Comments

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)?
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; }
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/1
this one is coo, I would just adjust the yiq to >= 160, worked better for me.
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/

1 Comment

This is not what the OP is asking for. This inverts the colour, if you have a green background, you get orange text.
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+')'); 

8 Comments

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.
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).
What if the background colour is #808080!?
@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.
|
10

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>

Comments

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>

Comments

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)

2 Comments

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.
Hello Jørgen, I think the colourBrightness script may serve your purpose: github.com/jamiebrittain/colourBrightness.js
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)) 

Comments

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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.