DEV Community

Cover image for Today I learned how to animate a text gradient in CSS (and JavaScript)
Thomas C. Haflich
Thomas C. Haflich

Posted on • Updated on

Today I learned how to animate a text gradient in CSS (and JavaScript)

Cover photo by Clem Onojeghuo via Unsplash.

I'm very sorry. And you're welcome.

Look at that bad boy chug. I think I set my CPU on fire making it... poor thing is really doing its best. I see why the CSS overlords didn't want to let me do this now.

Part one: Getting a text gradient 🌈

You might notice this part of the code:

@mixin lead($one, $two, $three, $four, $five, $six) {
  background: linear-gradient(80deg, $one, $two, $three, $four, $five, $six);
  background-clip: text;
  text-fill-color: transparent;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}
Enter fullscreen mode Exit fullscreen mode

That's what's generating the actual "rainbow text" part of this disaster.

The background part is just generating the gradient itself; the good old rainbow barf you might have seen elsewhere before. Removing all the background-clip and text-fill shenanigans in that mixin makes it look like this:

The text "happy pride month" in black with a rainbow gradient background.

It's all that other stuff that makes the text look like the background.

Normally the background-clip property is used to fine-tune the appearance of background around borders and paddings and the like, but the "text" value is highly magical.

The text-fill-color is roughly equivalent to your standard color attribute. (For this pen you could just as easily substitute color: transparent, I tried and it worked.) In this case we can set it to "transparent" so that our background shows through.

So what's going on here to get the "rainbow text" is:

  • The background is turned into rainbows
  • The background is clipped so that the only part of it that shows is the part that would normally be covered by text
  • The text is made transparent so we can see the background through it

Part two: You can animate a background, but you can't make it linear-gradient

This is where everything started to go off the rails. My first approach was to slap a transition on a single container and call it a day; it would have taken about five minutes, and most of that was googling how to make the gradient clip to the background.

However

Time to try another tactic.

<div id="main" class="container lead-red">
  HAPPY PRIDE MONTH
</div>
<div id="fade" class="container lead-orange">
  HAPPY PRIDE MONTH
</div>
Enter fullscreen mode Exit fullscreen mode

Wait, why are there two-

πŸ€”

function intervalFunction() {
  rainbowify();
  setTimeout(intervalFunction, getNextTimeoutDuration());
};

intervalFunction();
Enter fullscreen mode Exit fullscreen mode

What are you doing on an interval-

πŸ€” ... πŸ€” ... πŸ€” ...

Oh no.

Here's the gist:

Have two nearly identical HTML elements overlap each other. The first one, #main, is on the bottom layer and is always visible; it "blinks" between gradients at a constant opacity. The second one, #fade, is on the top layer and is constantly blinking in (when aligned) and fading out (to achieve an appearance of transition using opacity).

These two are not on the same "rainbow cycle" - the #fade text is ahead of the #main by one color. JavaScript runs a loop using setInterval to juggle the classes on these two elements to keep the colors moving.

That also didn't work.

Part three: Blink in, fade out

My code looked something like this: During the main setInterval loop, attempt to halt animation with a .halt class that sets the transition-timing property to 0ms (effectively disabling transitions). I would then set the opacity to 1 to get it to "blink in", and remove the .halt class. The I would set the opacity back to 0 and let the transition do its magic. This all happened immediately, on about four lines of code.

Well, CSS transitions do not work that way. It turns out that in order for it to transition, the rendering engine needs a couple milliseconds to get its act together, regardless of what the transition property was on the element at the time.

Adding and then removing a class almost immediately is not, as it turns out, enough time.

I messed around with transition timing and other CSS for awhile before giving up and trying JavaScript. The initial JS hack of using a setTimeout( ... , 20) inside my existing setInterval loop worked... about 95% of the time. Setting the timeout lower would cause it to stutter as the transitions couldn't keep up, and setting the timeout higher would cause highly noticeable pauses in the animation. However, having weird magic numbers lying around and occasional visits from Blinky McBlinkerton weren't where I wanted to leave it...

Part four: Reducing the juddering

The first part I wanted to eliminate was that magic 20ms timeout. I googled for an eternity and came up with this:

Trigger a reflow in between removing and adding the class name.

That explains this bit:

fade.classList.add("halt");
fade.classList.add("hide");
fade.classList.remove("lead-" + rainbow[(i + 1) % ilen]);
fade.classList.add("lead-" + rainbow[(i + 2) % ilen]);

void fade.offsetWidth; // <- this one!

fade.classList.remove("halt");
fade.classList.remove("hide");

Enter fullscreen mode Exit fullscreen mode

The next weird thing I figured I'd plan ahead for was JS timer drift. I've seen this before when building schedules and clocks; one millisecond as specified is not always one millisecond in reality, so any interval will inevitably drift further and further away from accuracy. Since I have the same timeout hardcoded into my SCSS and my JS, it would definitely look nicer if they could consistently line up. This could prevent further pauses, stutters, etc. due to timer drift.

To do this, I use setTimeout instead of setInterval, and have the timeout call a function that calls another timeout (effectively creating an interval out of timeouts). Each timeout notes when it starts, and notes the "drift" from the last timeout, and corrects itself to attempt to more accurately hit a long-term target. This would definitely be useful if I were to pivot to something like @keyframes.

In conclusion:

Thomas the tank engine side-eyeing the distance, captioned "Thomas had never seen such a mess."

This is unnecessarily complicated and runs like molasses in winter. Just make a gif or something.

(ETA: Follow-up here. If for some reason you want to actually do this...)

But I did it, CSS overlords. I've beaten you. I've won.

Top comments (8)

Collapse
 
erinjzimmer profile image
🌈 Erin Zimmer

You can create a very similar effect with less shenanigans by animating the background-position. You set the background-size to double the width of the element, then move it, like so

Obviously my animation is a little different as it runs forwards and backwards, instead of just in one direction, but I'm pretty confident you could work out how to make it go all in one way.

Collapse
 
tchaflich profile image
Thomas C. Haflich

If I wanted to use your solution without modifying the animation itself, I'd probably try to constrain the gradient a little, because you get a lot of the green / yellow / orange part of the spectrum and not much of the red and purple parts.

For the unidirectional method, I'm thinking probably keyframes, so nice segue into CSS animations. I was already thinking of doing a part 2, so you gave me a pretty good idea for some other topics to hit on there.

Collapse
 
desi profile image
Desi

I LOVE THIS!

Collapse
 
val_baca profile image
Valentin Baca

Love it πŸ³οΈβ€πŸŒˆ

Collapse
 
andreasjakof profile image
Andreas Jakof • Edited

Yes you did it! Next step: In WASM?Maybe it’s a litter less CPU hungry ?

But Kudos for all the hoops you had go around!

Collapse
 
diek profile image
diek

Wow, epic. I'm giving you an unicorn and a page mark, i need to read this in detail.

Collapse
 
tonixx profile image
tonixx

Loving it! I've saved it for a future project

Collapse
 
osde8info profile image
Clive Da

when you said "here's the gist" i thought you meant "here's the gist" shows just how much github have already brainwashed me