How to actually animate a text gradient in CSS
Tags: css, webdev
Published: 2019-06-22 23:29:27

*Cover photo by Kristopher Roller via Unsplash.*

Since you all liked my [iridescent shambling monstrosity](https://dev.to/tchaflich/today-i-learned-how-to-animate-a-text-gradient-in-css-and-javascript-2ehp), I figured it was worth a second swing. Thanks to [@erinjzimmer](https://dev.to/erinjzimmer) for letting me know that it's possible to animate the gradient background by animating `background-position`.

So, instead of writing a humorous article where I make the worst technically-functional CSS possible, let's try to make this thing not terrible. I swear, the only JavaScript I used this time was for the buttons. 

I don't have a problem, I can stop whenever I want.

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

Since we figured out how to get the rainbow text last time, let's jump to the good parts.

## Animating background position

To use transitions with this, it's very likely that I would have had to resort to similar tactics as in the previous post (*yikes*) to make sure that the gradient moved in one direction continuously. But instead of going through all that effort, I decided to use `@keyframes`, which turned out to be as easy as forgetting to add files before you commit.

```scss
@keyframes whoosh {
  0% { background-position-x: 0 }
  100% { background-position-x: 100% }
}

.animated-thing {
  animation-name: whoosh; // references @keyframes name
  animation-duration: 2000ms; // or whatever
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

```

There's an important caveat to be mentioned to make this look good, though.

{% codepen https://codepen.io/tchaflich/pen/QXvwJR %}
*A... catveat? Eh?*

The cat image is 200px wide, and the window is either 400px wide or 500px wide. (Click to toggle.) When the container is 400px wide, the animation is smooth and you don't notice it suddenly resetting to the beginning when it loops. When it's 500px wide, the end of the animation is 100px offset and the jump is *very* noticeable.

Using gifs for illustration:

![A cat walking on a spinning chair. The gif loop is very good.](https://media.giphy.com/media/82CItLnbSh8hzsXK3H/giphy.gif)

When the loop resets, it's in the same position as when it started, making the looping is much less noticeable. (There may still *be* a jump, but it should be very small.)

![A fluffy white cat sitting in a tree. At the last minute, the camera turns downward.](https://media.giphy.com/media/U7P0B9vQmKNZFzDr6e/giphy.gif)

Despite the cuteness of this fluffy cloud-friend, the jump is very noticeable due to the camera turning downward sharply at the last moment. Would pat cat, would not animate smoothly.

Another issue is that setting the background to be *exactly* the same width as its container in this case causes it to not animate. I guess CSS thinks it has nowhere to go! So it needs to be some exact fraction or exact multiple, but not *equal*. And it needs to reset to the exact same position it started in.

## Animating background position *with gradients*

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

To make CSS actually animate it, I set the `background-size` to 200% width (and 100% height). That was the easy part.

To get the gradient to animate *smoothly* was trickier.

```scss
  $first-color: #996699;
  $second-color:  #B39500;
  $third-color:  #009980;
  $fourth-color: #006699;
  $angle: 75deg;
  
  background-image: linear-gradient(
    $angle,
    // so you can see the repetition of colors easily
    $first-color,
    $second-color,
    $third-color,
    $fourth-color,
    // repeat a second time
    $first-color,
    $second-color,
    $third-color,
    $fourth-color,
    // back to the first color
    $first-color
  );
}

```

This is how the gradient is set up so that it doesn't appear to "jump" when the loop resets. It needs to repeat twice, but also repeat the first bit of the loop. In the final pen, this was turned into a mixin.

```scss
@mixin flag-gradient($direction: null, $color-stops...) {
  $grads: $color-stops;
  @each $stop in $color-stops {
    $grads: append($grads, $stop);
  }
  $grads: append($grads, nth(nth($grads, 1), 1));

  // [ clipped ]
  
  background-image: linear-gradient($direction, $grads);
}
```

This does all that repetition work for you. It copies the arg list sent in, then loops over it to append each of the new color stops (hmu if you know a better way to do that part), Then picks out the color part of the first color stop and tacks that onto the end. That way you only need to specify your actual flag colors.

Also take note of the angle you put the gradient towards - the steeper it is, the more noticeable the jump will be. Somewhere between 75 and 90 is nearly invisible but still looks tilty enough to be aesthetically pleasing. The smaller the width the sharper this angle needs to be, as well. 90 is safe!

If you're viewing the inline pen on dev.to, you'll see the 90 degree variant, because of this bit:

```scss
@mixin flag-gradient($direction: null, $color-stops...) {
  // [ clipped ]
  
  @if $direction == null {
    $direction: 75deg;
    @media only screen and  (max-width : 800px) {
      $direction: 80deg;
    }
    @media only screen and  (max-width : 500px) {
      $direction: 90deg;
    }
  }
  
  // [ clipped ]
}
```

Since the animation quality varies so sharply based on the angle & width, I auto adjusted it if you don't send an override.

## Adding more flags 🏳️‍🌈 (refactoring)

After I got the initial rainbow animation going, it seemed... too easy. I needed to work harder in the eternal struggle against CSS. I wondered, was this able to be generalized? Could I show *even more pride*?

![A code screenshot showing the bi pride flag using multiple colors in a row for color stops.](https://thepracticaldev.s3.amazonaws.com/i/wq4lbxq9kusqsbl8wleh.PNG)

In the bi pride flag, I discovered another interesting caveat. In order to make the jumps less noticeable, the gradient should be as smooth as possible. Using percentage based color stops generally turns out poorly; just plain duplicating the colors turned out to have better results.

![Trans pride flag colors (blue, pink, and white) against a white background.](https://thepracticaldev.s3.amazonaws.com/i/bt2jyk8b1wd14h8c2866.png)

Hm, I'd really like to see a text shadow on there to make the white bits not blend into the background so much. White shows up on a few different flags, so it's definitely a general issue.

![The text is now covered entirely in darkness. Oh no!](https://thepracticaldev.s3.amazonaws.com/i/x6hu8uw9c1rn821x0qlh.png)
*That went well.*

This is actually entirely logical, if you think about it. The entire reason the gradient text trick works in the first place is by making the text itself transparent, and clipping the background; not by actually *applying* the background to the text. 

I did look into fixing this. [StackOverflow](https://stackoverflow.com/a/28750655) suggests:

> Basically, the idea is to have 2 div overplayed on each other. The lower one provides the background and the shadow. And the the div above just uses a mask to cut out the exact same text from the background image, so that it covers the text of lower element, but not the shadow:

#### 2 div overplayed on each other

### 2 div overplayed on each other

## 2 div overplayed on each other

![A gif of a dog hopping away, with lots of "nope" text overlaid.](https://media.tenor.co/images/60d6ac6564c529d7c964aa9e66c62bce/raw)

Not going down that road again! You can't make me!

![The text with trans-pride gradient now has a dark background](https://thepracticaldev.s3.amazonaws.com/i/civo96oh90vugll6ajqq.png)

Close enough.

After a bit of playing around with various flag designs and colors, I settled on needing four different "themes", based around light/dark and warm/cool. That's what all this is for:

```scss
$background-map: (
	light-cool: #ecf0f1,
	light-warm: #f2eded,
	dark-cool: #10181e,
	dark-warm: #0e070a,
);

@function get-opposite-tone($tone) {
  @if $tone == 'light' {
    @return 'dark';
  }
  @return 'light';
}

@mixin set-background($tone: 'light', $temp: 'cool') {
  $primary: null;
  $secondary: null;
  
  background-color: map-get($background-map, #{$tone}-#{$temp});
  & > #picker > button {
    background-color: map-get($background-map,
      #{get-opposite-tone($tone)}-#{$temp}
    );
    &.highlight {
      @if $tone == 'light' {
        background-color: lighten(map-get($background-map,
        #{get-opposite-tone($tone)}-#{$temp}
      ), 12);
      } @else {
        background-color: darken(map-get($background-map,
          #{get-opposite-tone($tone)}-#{$temp}
        ), 24);
      }
    }
    color: map-get($background-map, #{$tone}-#{$temp});
  }
  
  // [ clipped ]
}
```

At this point, all I needed was 8 lines of JavaScript (... plus jQuery) to hook the buttons up.

![Frodo from LOTR in front of the volcano: "It's done."](https://media.giphy.com/media/zCME2Cd20Czvy/giphy.gif)

### Wrap-up fun facts

* The buttons on the side use [CSS Grid](https://developer.mozilla.org/en-US/docs/Web/CSS/grid) for layout and it was extremely easy and convenient
* Did you know there's not one standard "lesbian" pride flag? I picked the lipstick lesbian flag because that was easy to find color definitions for and seemed to be the most popular
* SCSS [arglists](https://www.sitepoint.com/sass-multiple-arguments-lists-or-arglist/) are a thing
* You can't put an `@media` query statement inside an `@function` (lmao I tried just to see if it *would* work, but the compiler throws an exception)


![Dazzling, luminescent glitter text reading "peace out homies"](https://thepracticaldev.s3.amazonaws.com/i/yqjqx5hp8n7oiy2swelh.gif)