CSS mix blend mode / only white or black

13.2k Views Asked by At

Hello I'm currently trying to find a solution to make a sticky text black if the background is white or black in any other case. During my research I found mix blend mode property but it seems very complex to make what I want.

.sticky {
  position: -webkit-sticky;
  position: sticky;
  top: 20px;
  color:white;
  font-size:60px;
  mix-blend-mode: difference;
}
.panel {
  height: 80vh;
  widht: 100%;
}

.bg-black {
  background: black;
}

.bg-red {
  background: red;
}

.bg-blue {
  background: blue;
}

.bg-green {
  background: green;
}
<div class="sticky">
  My text
</div>
<div>
  <section class="panel"></section>
  <section class="panel bg-black"></section>
  <section class="panel bg-red"></section>
  <section class="panel bg-blue"></section>
  <section class="panel bg-green"></section>
</div>

Does someone know a hack or a package that can help me?

Thanks a lot

3

There are 3 best solutions below

0
On BEST ANSWER

I finally find something great ! It's not as beautiful as mix-blend-mode but it do the job.

I'd prefer to stay 100% css because it require use of ScrollMagic

const controller = new ScrollMagic.Controller();
const sections = document.querySelectorAll('section');
const menu = document.querySelector('.my-text');


sections.forEach((section, index, arr) => {
  const trigger = '#' + section.id;
  const backgroundColor = window.getComputedStyle(section, null).getPropertyValue('background-color');
 
  const textColor = getContrastYIQ(backgroundColor);

  let previousBackgroundColor = backgroundColor;
  let previousTextColor = getContrastYIQ(previousBackgroundColor);
  

  if (index >= 1) {
    previousBackgroundColor = window.getComputedStyle(arr[index - 1], null).getPropertyValue('background-color');
    previousTextColor = getContrastYIQ(previousBackgroundColor);
  }

  new ScrollMagic.Scene({
      triggerElement: trigger,
      triggerHook: "onLeave",
      offset: -50,
      reverse: true
    })
    .on("enter", function() {
      menu.classList.remove(previousTextColor);
      menu.classList.add(textColor);

    })
    .on("leave", function() {
      menu.classList.remove(textColor);       menu.classList.add(previousTextColor);

    })
    .addTo(controller);
})

// Color contrast helper function
// https://en.wikipedia.org/wiki/YIQ
function getContrastYIQ(rgb) {
  rgb = rgb.substring(4, rgb.length - 1)
    .replace(/ /g, '')
    .split(',');
  const yiq = ((rgb[0] * 299) + (rgb[1] * 587) + (rgb[2] * 114)) / 1000;
  return (yiq >= 128) ? 'black' : 'white';
}
section {
  min-height: 80vh;
}

.my-text {
  position: sticky;
  top: 5vh;
  color: white;
}

.black {
    color: black;
    &:before {
      background: black;
      box-shadow: 0 0.4em 0 0 black, 0 0.80em 0 0 black;
    }
 }

#s1 {
  background-color: black;
}

#s2 {
  background-color:  white;
}

#s3 {
  background-color: #111;
}

#s4 {
  background-color: #9f3;
}

#s5 {
  background-color: #145;
}

#s6 {
  background-color: #f5f; 
}
<script ></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.5/ScrollMagic.min.js"></script>
<div class="my-text">
  MY TEXT</div>
<section id="s1">
</section>
<section id="s2"></section>
<section id="s3"></section>
<section id="s4"></section>
<section id="s5"></section>
<section id="s6"></section>

1
On

I'm not sure it's possible with mix-blend-mode, can do what you want with filter and background-clip: text though:

// can ignore this, it's just making the sliders work as R G B
function updateColor() {
  const r = document.getElementById('r').value;
  const g = document.getElementById('g').value;
  const b = document.getElementById('b').value;
  
  document.querySelector('.container').style.background = `rgb(${r},${g},${b}`;
}
.container {
  background: white;
}

.contrast-text {
  font-size: 50vmin;

  background: inherit;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter: 
    sepia(5)      /* add some color to grey so the rest works */
    saturate(100) /* increase strength of color channels */
    invert(1)     /* invert the color */
    grayscale(1)  /* make it grey */
    contrast(9);  /* make it black/white */
}
<input type="range" onchange="updateColor()" min=0 max=255 value=255 id="r">
<input type="range" onchange="updateColor()" min=0 max=255 value=255 id="g">
<input type="range" onchange="updateColor()" min=0 max=255 value=255 id="b">

<div class="container">
  <div class="contrast-text"> Text </div>
</div>

0
On

It is possible to make it with CSS only, by not applying blend mode to the sticky elements, but to the background(::before, ::after) items.

.bg-field::before, .bg-field::after {
  background-color: white;
  mix-blend-mode: difference;
  pointer-events: none;
  content: "";
  position: absolute;
  bottom: 0;
  top: 0;
  right: 0;
  left: 0;
}

.bg-field::before {
  z-index: 1;
}

.bg-field::after {
  background-color: red;
  z-index: -1;
}

Everything inside the bg field (or even outside if its a fixed element) will be colored

I made a code snippet displaying how it works: https://codepen.io/AndrewKnife/pen/XWzBpeN