MetaFizzy Effect with Sass

Avatar of Kitty Giraudel
Kitty Giraudel on (Updated on )

The following post is by Kitty Giraudel, a regular contributor here. In the grand CSS-Tricks tradition, Hugo found a cool effect on the web and dug into how he could re-create it in a smart way.

A couple of days ago, I saw this pen by Hugo Darby-Brown which intented to reproduce the MetaFizzy effect by David DeSandro in pure CSS with the help of Sass. Hugo did a great job and probably the most important one: digging into the original JavaScript to understand how to do it in Sass.

He succeeded, but I thought his code could be improved. Even if it was clearly far better than a vanilla CSS version, there was still a lot of repeated code. Let’s find a way to make it like super-DRY!

Important! This is really just an experiment. If you ever want to do something like this, please just use JavaScript. The CSS version takes like 500 lines long and turns out to be quite heavy for the GPU. So once again, this is a Sass experiment. Just for fun.

Where do we start?

This is a question I would not be able to answer if Hugo didn’t do his demo first. Here’s what we need to do:

  • Give our text a long shadow, progressively fading to black
  • Make the color of the shadow slowly change over time
  • On hover, go crazy rainbow
  • Animate this rainbow quickly

So in the end, we will need a couple of things:

  • An animation for the smooth shadow
  • An animation for the hover state
  • A list of colors for the hover state

That’s pretty much it.

The smooth shadow

The keyframes

The smooth shadow is the easier of the things we’ll do. All we do is output a bunch of text-shadows of one color progressively fading them to black. Then, we need an animation which will change this color over time. A perfect job for hsl()!

Because we want our Sass code as DRY as possible, we’ll call a mixin in our keyframes that will handle the crapload of text-shadows for us. First, the skeleton.

@mixin text-3d($hue) {
  /* Output crazy text-shadows */
}

@keyframes text-3d-animation {
  @for $i from 0 through 10 {
    #{$i * 10%} {
      @include text-3d($i * 36);        
    }
  }
}

All we did was create a text-3d-animation CSS animation with 11 explicit keyframes (0%, 10%, … 90%, 100%). In each keyframe, we call a mixin named text-3d, passing $i * 36 as $hue argument (36, 72, 108, 144, 156, …). If you are familiar with hsl() notation, you can see where this is going.

The mixin

Now we’ve created the animation calling the mixin, it’s time to build the mixin! All it has to do is outputing a bunch of text-shadows. I went with 50 which is pretty huge already but you can pick the number you want I guess (although you have to hardcode this, I didn’t define a parameter for this). Once again, we won’t write our shadows manually; Sass lists and loops are meant for this.

@mixin text-3d($hue) {
  $ts: ();
  @for $i from 1 through 50 {
    $ts: $ts, $i*2px $i*2px hsl($hue + $i*1, 100%, 50% - $i);            
  }
  text-shadow: $ts, 0 0 50px, 0 0 55px;
}

Don’t panic yet! This is actually simple. Before entering our loop, we define an empty list called $ts (stands for text-shadow). Then we enter the loop. In each run, we append a new shadow to our list where:

  • both horizontal and vertical offsets are set to $i * 2px to make the shadows bigger and bigger
  • we don’t define any blur, but you can set one if you like
  • the color is defined in HSL with hue set to the given parameter (multiple of 36) + $i * 1, the saturation to 100% and the lightness to 50% - $i, meaning it progressively goes to black

Then once the loop is finally over, we simply output our $ts list as a value for text-shadow. We also add two shadows manually for the cool white “aura”.

And we’re done for the non-hover MetaFizzy effect! It should work like a charm.

The crazy rainbow

For the most part, the hover animation works the same as the non-hover animation. We will proceed the same way we did before, starting with the keyframes.

The keyframes

@keyframes crazy-rainbow-animation {      
    @for $i from 1 through 50 {
      #{$i * 2%} {
        @include crazy-rainbow($i, tomato yellow green blue purple);
      }
    }
  }

As you can see, this is pretty much the same thing we used for the 3D text animation except we won’t use 11 explicit keyframes here but 50. Actually 51 if we want to prevent a little glitch; let’s add the 0% keyframe (out of the loop of course).

@keyframes crazy-rainbow-animation {      
  0% {
    @include crazy-rainbow(50, tomato yellow green blue purple);
  }
  @for $i from 1 through 50 {
    #{$i * 2%} {
      @include crazy-rainbow($i, tomato yellow green blue purple);
    }
  }
}

We pass our crazy-rainbow mixin two parameters:

  1. $i as a numeric value once again (we’ll see the point later)
  2. the list of colors we want to see moving when hovering the text (that’s right, we can customize the colors!)

The mechanics

This is where things get complicated. The animation when hovered basically looks like a striped shadows (which makes no sense anymore): one color, then another one, then an other color, and so on… But there’s more, the color are moving.

The idea is to have something like this:

@keyframes crazy-rainbow-animation {
  0% {
    text-shadow: 2px 2px   color1, 4px 4px   color1, 6px 6px   color1, 8px 8px   color1,
                 10px 10px color2, 12px 12px color2, 14px 14px color2, 16px 16px color2,
                 18px 18px color3, 20px 20px color3, 22px 22px color3, 24px 24px color3;
  }

  2% {
    text-shadow: 2px 2px   color3, 4px 4px   color1, 6px 6px   color1, 8px 8px   color1,
                 10px 10px color1, 12px 12px color2, 14px 14px color2, 16px 16px color2,
                 18px 18px color2, 20px 20px color3, 22px 22px color3, 24px 24px color3;
  }
  
  /* And so on... */
}

At every new keyframe, colors (not offsets) have to be moved 1 index in the list. The last color of the list comes at first, and every color is pushed of one slot to the right. So in the end, we have the same number of shadows with the same offsets except their color change.

Building the array of colors

Because of this, we need a list of colors as long as the number of shadows we want to output. If we want to use 50 shadows, we need a list of 50 colors. Creating this list manually would be a pain in the butt, so we create a function for this.

The goal of this function is to turn a list of colors into a list of colors. But the returned list should match the length we want so we can turn a list of 5 colors into a list of 50. Like this:

$given-colors: tomato yellow green blue purple;
$returned-colors: create-list($colors);
/*
$returned-colors: tomato, tomato, tomato, tomato, tomato, tomato, tomato, tomato, tomato, tomato,
                  yellow, yellow, yellow, yellow, yellow, yellow, yellow, yellow, yellow, yellow, 
                  green, green, green, green, green, green, green, green, green, green, 
                  blue, blue, blue, blue, blue, blue, blue, blue, blue, blue, 
                  purple, purple, purple, purple, purple, purple, purple, purple, purple, purple;
*/

Unfortunately, I realized 50 shadows is not always a good number for this animation. It occurred to me depending on the number of colors you want to run, the animation may not be quite well completed; sometimes the colors just “jump”. This is because we need the shadows from the last keyframe to match the shadows from the first.

To sum up, we need to find a number which is:

  • lesser than or equals to 50 (number of keyframes)
  • a multiple of the length of the color list (to make the animation loop without any jump)
  • the closest to 50 as possible (to make the animation as fluid as possible)

So let’s say we have a list of 6 elements, the function should return 8 (because 9 would go over 50 since 9 * 6 = 54). A list of 7 elements should return 7 (because 7 * 7 = 49). You get the idea.

@function define-max($n) {
  @for $i from 1 through 50 {
    @if $i * $n > 50 {
      @return $i - 1;
    }
  }
}

Now back to our create-list() function. We have a list of a couple of colors and want to turn it into a list of about 50 colors? Okay, great.

@function create-list($colors) {
  $max: define-max( length($colors) );
  $l: ();
  @each $c in $colors {
    @for $i from 1 through $max {
      $l: append($l, $c);
    }
  }
  @return $l;
}

The mixin

Okaaaaaay! So all we did until now was create a function to turn a list of colors into a longer list of colors. Let’s dig into the mixin.

@mixin crazy-rainbow($n, $colors) {
  $colors: create-list($colors);
  $ts: (); 
      
  @for $i from 1 through length($colors) {
    $n: if($n > length($colors) or $n == 0, 1, $n);
        
    $ts: $ts, $i*2px $i*2px 0 nth($colors, $n);
        
    $n: $n + 1;
  }
      
  text-shadow: $ts;
}

Same as earlier, we define a $ts empty list to store all our shadows. Then we enter the loop to add a shadow to our $ts list every time before moving the pointer of 1 index to the right ($n: $n + 1). If the index goes out of list range, we move it back to 1.

It works! Let’s sum up what we did:

  1. We computed the number of shadows (X) we needed to output based on the number of colors we wanted to run. This is only to make the animation loop correctly without any visual glitch.
  2. We generated a big array of X colors based on the number we previously computed and on the list of colors we wanted to run.
  3. At every keyframe we output X shadows starting from a different index in the array every time. This is what makes the colors move.

Improvings bits

Now that we’re done with all the mechanics, we can improve things a little bit. Why not make a metafizzy mixin assigning a couple of styles to our element?

@mixin metafizzy($size, $duration: 10s) {
  font-family: 'MetafizzyLogoRegular', cursive;
  color: white;
  line-height: .9em;
  font-weight: normal;
  font-size: $size;
  animation: text-3d-animation $duration linear infinite;  
  
  &:hover {
    animation: crazy-rainbow-animation 1s linear infinite; 
    animation-direction: reverse; 
  }
}

This mixin defines all the typography stuff, including the font family (you’ll need font files though), font size, font weight, line height and so on.

Let’s keep going. What about a mixin to generate our two keyframes animations? We could pass it the color list we want to use on hover.

@mixin metafizzy-animations($hover-colors) {
  @keyframes text-3d {
    @for $i from 0 through 10 {
      #{$i*10%} {
        @include text-3d($i * 36); 
      }
    }
  }  

  @keyframes crazy-rainbow {      
    @for $i from 1 through 50 {
      0% { 
       @include crazy-rainbow(50, $hover-colors); 
      } 
      #{$i*2%} {
       @include crazy-rainbow($i, $hover-colors);
      }
    }
  }
}

Unfortunately, we can’t include this mixin in the metafizzy one since the latter is included inside a selector (like h1). Actually we can do it, but this won’t work; the @keyframes animations will be outputed inside the selector and not at root of document.

In Sass 3.3, we will have the @at-root directive that will make this kind of thing possible (@directive bubbling), but for now this isn’t possible so we have to include it at root.

Usage & demo

@include metafizzy-animations(red orangered yellow lightgreen green deepskyblue);

h1 {
  @include metafizzy(25em, 5s);  
  /* Other styles that please you */   
}

Done. That’s all I got folks, hope you liked it and thanks for reading!