Changing text color dynamically depending on the background image

Let's assume that you are working with the following HTML skeleton:

html
<div class = 'Background'/>
  <h2>This a text with high contrast</h2>
</div>

The goal is to set a background image using CSS and render the text color in black or white, depending on the average color of the region behind the text.

CSS
.Background{
  align-items: center;
  background: url('https://images.unsplash.com/photo-1621868579917-560fc2c68ec0?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=675&q=80');
  display: flex;
  height: 600px;
  justify-content: center;
  padding: 20px;
  width: 600px;
}

Main thread

The steps to follow are:

  1. Get text & background
  2. Calculate text & background measures (size & position)
  3. Cut the background image behind the text
  4. Draw the portion of the background on a new canvas
  5. Calculate the YIQ based on the average RGB
  6. Decide text color depending on the YIQ
javascript
const changeTextColor = () => {
  
    // 0. Get text and background
    let text  = document.getElementsByTagName('h2')[0];
    let bckg  = document.getElementsByClassName('Background')[0];
    
    // 1. Calculate text and background measures
    let text_measures = text.getBoundingClientRect();
    let bckg_measures = bckg.getBoundingClientRect();
    
    // 2. Cut the background image behind the text
    let img = new Image();
    img.crossOrigin = 'anonymous';
    img.src         = window.getComputedStyle(bckg, false).backgroundImage.slice(5, -2);
    img.height      = bckg_measures.height;
    img.width       = bckg_measures.width;
    
    img.onload = () => {
        
        // 3. Draw the portion of the background on a new canvas
        let imgData = paintCanvas(img, bckg, bckg_measures, text, text_measures);
    
        // 4. Calculate the YIQ based on the average RGB
        let yiq = getYiq(imgData);

        // 5. Decide text color depending on the YIQ 
        text.style.color = yiq >= 128 ? 'black' : 'white';

    }

}

Additionally, you need to define the 2 functions to paint the canvas paintCanvas() and to get the YIQ getYiq().

Paint the canvas

To get the average RGB color of the background behind the text, you need to draw the image on a canvas and return the image data:

javascript
const paintCanvas = (img, bckg, bckg_measures, text, text_measures) => {
          
    // Creating the canvas element to draw the image
    let canvas = document.createElement('canvas');
    let ctx    = canvas.getContext('2d');

    // Setting the size of the canvas
    canvas.height = bckg_measures.height;
    canvas.width  = bckg_measures.width;

    // Drawing the final image
    ctx.drawImage(img, text.offsetLeft, text.offsetTop, text_measures.width, text_measures.height, 0, 0, text_measures.width, text_measures.height);

    // Getting the image data
    return ctx.getImageData(0, 0, text_measures.width, text_measures.height);

}

Get YIQ

Lastly, you can calculate the average RGB color and YIQ by reading each pixel of the canvas:

javascript
const getYiq = (imgData) => {
    
    // Calculating average color of canvas
    // Counting the total elements
    let rgb   = {r:0, g:0, b: 0};
    let count = 0;

    length = imgData.data.length;

    for (var i = 0; i < length; i += 4) {

        rgb.r += imgData.data[i];
        rgb.g += imgData.data[i + 1];
        rgb.b += imgData.data[i + 2];

        count ++;

   }

    // Calculating average
    rgb.r = Math.floor(rgb.r / count);
    rgb.g = Math.floor(rgb.g / count);
    rgb.b = Math.floor(rgb.b / count);

    // Getting the YIQ
    return yiq = ((rgb.r * 299) + (rgb.g * 587) + (rgb.b * 114)) / 1000;

}

You can see a full example here.

Hi, I'm Erik, an engineer from Barcelona. If you like the post or have any comments, say hi.