Today I learned how to animate a text gradient in CSS (and JavaScript)
Tags: css, webdev, javascript, todayilearned
Published: 2019-06-21 00:36:05

*Cover photo by Clem Onojeghuo via Unsplash.*

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

{% codepen https://codepen.io/tchaflich/pen/rEjrrE %}

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:

```scss
@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;
}
```

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.](https://thepracticaldev.s3.amazonaws.com/i/tp7qdpfcylxytg7ipxd3.PNG)

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

Normally the [background-clip property](https://developer.mozilla.org/en-US/docs/Web/CSS/background-clip) 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***

{% codepen https://codepen.io/tchaflich/pen/bPgmmB %}

Time to try another tactic.

```html
<div id="main" class="container lead-red">
  HAPPY PRIDE MONTH
</div>
<div id="fade" class="container lead-orange">
  HAPPY PRIDE MONTH
</div>
```

Wait, why are there two-

### 🤔

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

intervalFunction();
```

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](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions#JavaScript_examples). 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.](https://css-tricks.com/restart-css-animation/#article-header-id-0)

That explains this bit:

```javascript
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");

```

The next weird thing I figured I'd plan ahead for was [JS timer drift](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Reasons_for_delays_longer_than_specified). 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."](https://data.whicdn.com/images/300811981/large.png)

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

(ETA: Follow-up [here](https://dev.to/tchaflich/how-to-actually-animate-a-text-gradient-in-css-3a33). If for some reason you want to *actually* do this...)

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