CSS background-clip: text and opacity transition

409 Views Asked by At

I have a <h1> heading text that has each individual letter wrapped within a <span>, as so:

<h1 class='gradient-heading' id='fade'>
  <span>H</span>
  <span>e</span>
  <span>l</span>
  <span>l</span>
  <span>o</span>
</h1>

I would like to apply a gradient color to this heading text. As far as I know, the only way to do that is to use CSS background-clip: text and make the text inside transparent, something like:

.gradient-heading {
  background: linear-gradient(90deg, rgba(239,255,0,1) 0%, rgba(255,182,0,1) 48%, rgba(199,0,255,1) 100%);
  background-clip: text;
  color: transparent;
}

Now let's say that I want to fade in and out each individual <span> within the <h1> heading. With JavaScript this can be done with something like:

function fadeLetters() {
  const heading = document.getElementById('fade');
  const spans = heading.querySelectorAll('span');

  let delay = 0;
  let delayIncrement = 200;
  let numLettersFadedIn = 0;
  const totalLetters = spans.length;

  spans.forEach((span, index) => {
    setTimeout(() => {
      span.style.transition = 'opacity 1s ease-in-out';
      span.style.opacity = 1;
      numLettersFadedIn++;

      if (numLettersFadedIn === totalLetters) {
        setTimeout(() => {
          spans.forEach((span, index) => {
            setTimeout(() => {
              span.style.transition = 'opacity 1s ease-in-out';
              span.style.opacity = 0;
            }, index * delayIncrement);
          });
          setTimeout(() => {
            numLettersFadedIn = 0;
            fadeLetters();
          }, totalLetters * delayIncrement);
        }, delayIncrement);
      }
    }, delay);

    delay += delayIncrement;
  });
}

fadeLetters();

The problem is, this animation does not work properly. The fade-in/out actually does not happen. Letters blink on and off as if there was no transition. I believe this has to do with the background-clip: text CSS property, but I am not sure what exactly is the issue here.

Codepen that replicates this issue: https://codepen.io/mknelsen/pen/vYVxKeb

Has anybody done something similar before, or can anybody explain why this does not work properly?

2

There are 2 best solutions below

0
On BEST ANSWER

In the end what worked in my case was a different approach, I ended up using a trick similar to the last example in this CSS Tricks post: https://css-tricks.com/how-to-do-knockout-text/

function fadeLetters() {
  const heading = document.getElementById('fade');
  const spans = heading.querySelectorAll('span');

  let delay = 0;
  let delayIncrement = 200;
  let numLettersFadedIn = 0;
  const totalLetters = spans.length;

  spans.forEach((span, index) => {
    setTimeout(() => {
      span.style.color = 'rgba(255, 255, 255, 1)';
      numLettersFadedIn++;

      if (numLettersFadedIn === totalLetters) {
        setTimeout(() => {
          spans.forEach((span, index) => {
            setTimeout(() => {
              span.style.color = 'rgba(255, 255, 255, 0)';
            }, index * delayIncrement);
          });
          setTimeout(() => {
            numLettersFadedIn = 0;
            fadeLetters();
          }, totalLetters * delayIncrement);
        }, delayIncrement);
      }
    }, delay);

    delay += delayIncrement;
  });
}

fadeLetters();
body {
  font-family: system-ui;
  background: black;
  text-align: center;
}

h1 {
  background-color: black;
  border: 1px solid black;
  mix-blend-mode: lighten;
  position: relative;
}

h1::before {
  position: absolute;
  top: 0px;
  left: 0px;
  width: 100%;
  height: 100%;
  display: block;
  content: '';
  background: linear-gradient(90deg, rgba(239,255,0,1) 0%, rgba(255,182,0,1) 48%, rgba(199,0,255,1) 100%);
  background-size: 400% 400%;
  mix-blend-mode: multiply;
  animation: gradient 10s ease infinite;
}

@keyframes gradient {
  0% {
    background-position: 0% 50%;
  }

  50% {
    background-position: 100% 50%;
  }

  100% {
    background-position: 0% 50%;
  }
}

span {
  color: rgba(255, 255, 255, 0);
  transition: color 1s ease-in-out;
  font-size: 80px;
}
<h1 class='gradient-heading' id='fade'>
  <span>H</span>
  <span>e</span>
  <span>l</span>
  <span>l</span>
  <span>o</span>
</h1>

2
On

Changing the opacity of something that is already transparent to begin with doesn't sound right. I don't know exactly why.

Regardless, such an animation is possible, even without JS. What you need is to animate color from rgba(r, g, b, 0) (alpha 0, transparent) to rgba(r, g, b, 1) (alpha 1, original color) and vice versa alternately:

(See the vanilla CSS version in the snippet below.)

.gradient-heading {
  background: var(--background);
  -webkit-background-clip: text;
  background-clip: text;
}

span {
  animation: flash var(--duration) ease-in-out infinite alternate;
}

@for $i from 1 through 5 {
  span:nth-child(#{$i}) {
    animation-delay: calc(var(--delay-increment) * #{$i});
  }
}

@keyframes flash {
  0% {
    color: var(--color-0);
  }
  100% {
    color: var(--color-1);
  }
}

Try it:

:root {
  --duration: 1s;
  --delay-increment: 0.2s;
  --color-1: rgba(0, 0, 0, 1);
  --color-0: rgba(0, 0, 0, 0);
  --background: linear-gradient(90deg, #efff00 0%, #ffb600 48%, #c700ff 100%);
}

.gradient-heading {
  background: var(--background);
  -webkit-background-clip: text;
  background-clip: text;
}

span {
  animation-name: flash;
  animation-duration: var(--duration);
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  animation-direction: alternate;
}

span:nth-child(1) {
  animation-delay: calc(var(--delay-increment) * 1);
}

span:nth-child(2) {
  animation-delay: calc(var(--delay-increment) * 2);
}

span:nth-child(3) {
  animation-delay: calc(var(--delay-increment) * 3);
}

span:nth-child(4) {
  animation-delay: calc(var(--delay-increment) * 4);
}

span:nth-child(5) {
  animation-delay: calc(var(--delay-increment) * 5);
}

@keyframes flash {
  0% {
    color: var(--color-0);
  }
  100% {
    color: var(--color-1);
  }
}

/* Demo only */

body {
  font-family: system-ui;
  background: var(--color-1);
  text-align: center;
}

h1 {
  font-size: 70px;
}
<h1 class="gradient-heading">
  <span>H</span>
  <span>e</span>
  <span>l</span>
  <span>l</span>
  <span>o</span>
</h1>