2

I'm creating several divs in Javascript by inserting something like this

'<div style="background-color:' + bgColor + '</div>'

Now I want to set the color of the text automatically based on the luminosity of the background to black or white.

I see 2 options - all driven from Javascript or in CSS only. I prefer the CSS option, however, don't know how to read the background color for a CSS function, e.g.

@function set-color($color) {
  @if (lightness($color) > 40) {
    @return #000;
  }
  @else {
    @return #FFF;
  }
}

How can I fetch the background color to do something like this

div { color: set-color(???); }

How about mix-blend-mode ?

Benjamin E.
  • 5,042
  • 5
  • 38
  • 65

2 Answers2

3

There are several ideas put forward in the question about how to choose between black and white for text color depending on the background color.

Most have been answered one way or another in comments. Taking the ideas one by one:

Can we use CSS mix-blend-mode - no. There is no one setting for this that ensures text will appear readable on all possible backgrounds.

Can we use CSS (the preferred method) - unfortunately no as the div requiring the text color to depend on background color is created by JS at run time.

Can we use JS - yes and as the div is being created by JS and having its background-color set then it might as well have its color set then too.

The JS string is as given in the question with the addition of a setting for color:

'<div style="background-color:' + bgColor + '; color: ' + textBlackOrWhite(bgColor) + ';"'

Here is a snippet which defines the function. The snippet also lets you choose a background color and it then sets a color which (roughly) depends on the 'brightness' of the background. See the SO questions referenced here for further discussion as human color perception is a difficult topic.

//from https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
function textBlackOrWhite(hex) {
  // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
  var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
  hex = hex.replace(shorthandRegex, function(m, r, g, b) {
    return r + r + g + g + b + b;
  });

  let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);//also checks we have a well-formed hex number
  
//from https://stackoverflow.com/questions/596216 the answer by @FranciPenov which gives an approximation: (R+R+G+G+G+B)/6 
  let itsbright = function () { return ((2*parseInt(result[1], 16) + 3*parseInt(result[2], 16) + parseInt(result[3], 16))/6)>127; }
  return result ? (itsbright()) ? '#000000' : '#ffffff' : '#000000';//falls back onto black if bgColor was not a well-formed 3 or 6 digit hex color
}
<div id="div" style="font-family: monospace; box-sizing: border-box; margin: 0; padding: 40px 0; width: 100px; height: 100px; border-style: solid; border-radius: 50%; background-color: black; color: white;text-align:center;">#ffffff</div>
Click to choose background color: <input id="input" placeholder='#00000' type='color' value='#000000'/>
<button onclick="let d = document.getElementById('div'); let i = document.getElementById('input'); d.innerHTML = i.value; d.style.backgroundColor = i.value; d.style.color = textBlackOrWhite(i.value);">Submit</button>
A Haworth
  • 30,908
  • 4
  • 11
  • 14
1

I want to set the color of the text automatically based on the luminosity of the background to black or white.

I recognise this is a different approach from what you're asking for, but one CSS-only approach to having universally-readable text, regardless of background-color is to have white text with a black outline (or vice versa).

You can use 4 text-shadows for the outline:

text-shadow: 1px 1px rgb(0, 0, 0), -1px 1px rgb(0, 0, 0), -1px -1px rgb(0, 0, 0), 1px -1px rgb(0, 0, 0);

Working Example

const divs = [...document.getElementsByTagName('div')];

for (div of divs) {

  let redValue = Math.floor(Math.random() * 255);
  let greenValue = Math.floor(Math.random() * 255);
  let blueValue = Math.floor(Math.random() * 255);
  
  div.style.backgroundColor = `rgb(${redValue}, ${greenValue}, ${blueValue})`;
}
div {
  float: left;
  width: 180px;
  height: 40px;
  margin: 0 6px 6px 0;
  line-height: 40px;
  color: rgb(255, 255, 255);
  font-size: 32px;
  text-align: center;
  text-shadow: 1px 1px rgb(0, 0, 0), -1px 1px rgb(0, 0, 0), -1px -1px rgb(0, 0, 0), 1px -1px rgb(0, 0, 0);
  background-color: yellow;
}
<div>Sample Text</div>
<div>Sample Text</div>
<div>Sample Text</div>
<div>Sample Text</div>
<div>Sample Text</div>
<div>Sample Text</div>
<div>Sample Text</div>
<div>Sample Text</div>
<div>Sample Text</div>
<div>Sample Text</div>
<div>Sample Text</div>
<div>Sample Text</div>
Rounin
  • 27,134
  • 9
  • 83
  • 108
  • 1
    This is a really nice solution (being CSS only) for display-type text, headings and so on. It's not unreadable at say 16px but quite difficult to read a long text down at these lower font sizes maybe. Depends on a particular use case. – A Haworth Nov 14 '20 at 11:30
  • Thanks, @AHaworth. You're right, of course - this approach is better suited to some use-cases and less well suited to others. – Rounin Nov 14 '20 at 16:37
  • Yes, but the UX people might be upset :D – Benjamin E. Nov 15 '20 at 04:36
  • 1
    @BenjaminE. - Yes, possibly. I actually stole this technique from vintage LucasArts graphic adventure games (like _The Secret of Monkey Island_ from 1990). – Rounin Nov 15 '20 at 13:30