Debugging CSS Keyframe Animations

Avatar of Sarah Drasner
Sarah Drasner on

Creating CSS animations may be about learning the syntax, but mastering a beautiful and intuitive-feeling animation requires a bit more nuance. Since animations command so much attention, it’s important to refine our code to get the timing right and debug things when they go wrong. After tackling this problem myself, I thought I’d collect some of the tools that exist to aid in this process.

Using Negative Delay Values

Say you have multiple animations running at the same time and you’d like them to stagger just a bit. You can use animation-delay, but don’t necessarily want the viewer to visit the page and have some things not moving, waiting for their delay.

You can set animation-delay to a negative number, and it will push the playhead back in time, so that all animations are running when the viewer shows up. This is particularly useful when the animations share the same keyframe values and are differentiated in movement only by the delay.

You can use this concept for debugging. Set animation-play-state: paused; and then adjust the delay to different negative times. You’ll see the animation in different paused states along the tween.

.thing {
  animation: move 2s linear infinite alternate;
  animation-play-state: paused;
  animation-delay: -1s;
}

Example:

See the Pen LVMMGZ by CSS-Tricks (@css-tricks) on CodePen.

In a fun demo, we see the two robotters hovering at a very slightly different time in order to feel a little more natural. We give the purple robotter a negative delay when we declare the hovering animation so that he’s moving when the viewer first sees the page.

.teal {
 animation: hover 2s ease-in-out infinite both;
}
 
.purple {
 animation: hover 2s -0.5s ease-in-out infinite both;
}
 
@keyframes hover {
  50% {
    transform: translateY(-4px);
  }
}

See the Pen Robotter Love by Sarah Drasner (@sdras) on CodePen.

The Pains of Multiple Transform Values

For best possible performance, you should also be moving and changing things with transform, and save yourself from the cost of repaints with margin, or top/left and the like. Paul Lewis has a great resource called CSS Triggers that break down these costs in an easy-to-see table. The gotcha here is that if you try to move things with multiple transforms, there are a number of problems.

One big problem is ordering. Transforms will not be applied simultaneously as one might expect, but rather, in an order of operation. The first operation done is the one furthest on the right, then inwards. For example, in the code below, the scale will be applied first, then the translate, then the rotate.

@keyframes foo {
 to {
   /*         3rd           2nd              1st      */
   transform: rotate(90deg) translateX(30px) scale(1.5);
 }
}

In most situations, this is not ideal. It’s more likely that you’d rather all to happen concurrently. Furthermore, this becomes far more complex when you begin splitting the transforms into multiple keyframes with some values at the same time and some not, like so:

@keyframes foo {
  30% {
    transform: rotateY(360deg);
  }
  65% {
    transform: translateY(-30px) rotateY(-360deg) scale(1.5);
  }
  90% {
    transform: translateY(10px) scale(0.75);
  }
}

This will lead to somewhat surprising, and less than ideal results. The answer, unfortunately, is often to use multiple nested <div>s, applying a single translation to each, so that no conflicts arise.

See the Pen Show Order of Operations Transform Fail by Sarah Drasner (@sdras) on CodePen.

There are some alternatives, such as using matrix transforms (not intuitive to code by hand) or to use a JavaScript animation API such as GreenSock, where there is no ordering sequence for multiple transform interpolations.

The multiple div implementation can also help with troubling bugs with SVG. In Safari, you can’t declare opacity and transform in animation at the same time — one will fail. You can see the workaround in action in the first demo in this article.

In early August 2015, independent transform declarations have moved into Chrome Canary. This means we won’t have to worry about ordering much longer. You’ll be able to declare rotate, translate, and scale separately.

DevTools Timing Helpers

Both Chrome and Firefox now ship with some tools specificially for helping work with animations. They offer a slider for controlling the speed, a pause button, and UI for working with the easing values. Slowing things way down, and seeing the animation at particular stopped points is pretty darn helpful for debugging CSS animations.

They both use Lea Verou’s cubic-bezier.com visualization and GUI. This is extraordinarily helpful, as you no longer have to do the back-and-forth from cubic-bezier.com to text editor to proofing.

These tools allow us to fine-tune our animations much more intuitively. Here’s a look at the UI offered in both:

Both Chrome and Firefox allows you to control the timing (speed up or slow down), and also manually scrub through the animation. More advanced timeline tools are coming in Chrome that can look at multiple elements at at time. That will be nice, as working with only a single element at a time in an animation is fairly big limiting factor.

One thing I have some trouble with is grabbing the element quickly enough if the animation is short-lived and fast. In those cases, I’ll usually set it to animation-iteration-count: infinite; so that I can continue to play with it without having to fight time.

I also find it extremely helpful to slow the animation way down and then replay and adjust the timing in the browser with these tools. It allows you to pull apart each movement at a fundamental level and see how everything is interacting and what the motion progress looks like. If you refine it at that speed, when you adjust it to play faster, it will look much more masterful.

Debugging CSS Animation Events with JavaScript

If you’d like to figure out exactly where and when each animation is fired, you can use a little JavaScript to detect and alert you to when each event is occurring, by hooking into animationstart, animationiteration and animationend.

Here’s a demo of that:

See the Pen Showing how to Debug Animation Play States in JavaScript by Sarah Drasner (@sdras) on CodePen.

Keep Keyframes Concise

I often see people declare the same property and value at the 0% keyframe and 100% keyframe. This is unnecessary and leads to bloated code. The browser will take the properties value as the initial and ending value by default.

Meaning this is overkill:

.element {
 animation: animation-name 2s linear infinite;
}
 
@keyframes animation-name {
  0% {
   transform: translateX(200px);
 }
  50% {
   transform: translateX(350px);
 }
 100% {
   transform: translateX(200px);
 }
}
 

It could be written like this:

.element {
 transform: translateX(200px);
 animation: animation-name 2s linear infinite;
}
 
@keyframes animation-name {
  50% {
   transform: translateX(350px);
 }
}

DRY-ing Out Animations

Creating a beautiful and succinct animation usually means writing a very specific cubic-bezier() easing function. A fine-tuned easing function works similarly to a company’s palette. You have your own specific branding and “voice” in your motion. If you’re using this all over a website, (and you should, for consistency) the easiest way to do so is to store one or two easing functions in a variable, just as we do with our palettes. SASS and other pre/post processors make that simple:

$smooth: cubic-bezier(0.17, 0.67, 0.48, 1.28);
 
.foo { animation: animation-name 3s $smooth; }
 
.bar { animation: animation-name 1s $smooth; }

When animating with CSS keyframes, we want as much help from the GPU as possible. That means that if you’re animating multiple objects, you want a way to easily prepare the DOM for the incoming movement and layerize the element. You can hardware accelerate a native DOM element (not SVG) with CSS by using a standard declaration block. Since we reuse that on all of the elements we’re animating, it makes sense to save some typing, and add this in using a mixin or extend:

@mixin accelerate($name) {
 will-change: $name;
 transform: translateZ(0);
 backface-visibility: hidden;
 perspective: 1000px;
}

.foo {
  @include accelerate(transform);
}

Be careful. Offloading too many elements at once can cause an inverse effect and performance to tank. Most animations should be fine, but be aware of this if you’re using something like haml to generate tons of DOM elements.

Loops for Better Performance

Smashing Magazine recently published a great piece showing the work behind the fantastic project, Species in Pieces. In one particular section, the author goes into detail about how animating all of the pieces at one was causing performance problems. He states:

Imagine that you’re moving 30 objects at the same time; you are asking a lot of the browser, and it makes sense that this would create problems. If you have a speed of 0.199 seconds and a delay of 0.2 seconds on each object, you would fix the problem by moving only one object at a time. The fact that the same amount of total movement happens doesn’t matter: If the animation is done as a chain, performance is immediately improved by 30 times.

You can take advantage of Sass or other pre/post-processor for loops for this kind of functionality. Here’s a pretty simple one that I wrote to loop through nth-child:

@for $i from 1 through $n {
  &:nth-child(#{$i}) {
    animation: loadIn 2s #{$i*0.11}s $easeOutSine forwards;
  }
}

Not only that, but you can use them to stagger visual effects like color as well. (Hit replay to rerun the animation.)

See the Pen SASS for loops in CSS Animations by Sarah Drasner (@sdras) on CodePen.

Adjust Many Animations in Sequence

When you’re creating longer animations, the way to sequence multiple animations or events is usually to chain them together with progressive delays. So something like:

animation: foo 3s ease-in-out, bar 4s 3s ease-in-out, brainz 6s 7s ease-in-out;

But let’s say you’re making refinements, and you discover that the second count of the first animation should change. This affects the delays of everything following it, so let’s adjust our timing for each one after. No big deal.

animation: foo 2.5s ease-in-out, bar 4s 2.5s ease-in-out, brainz 6s 6.5s ease-in-out;

But now let’s add another animation and adjust the timing of the second one again (this kind of fine-tuning happens all the time when creating a really nice animation in production). Well, this is starting to become a little inefficient. If you do that 3 more times, it’s really inefficient.

Then imagine halfway through the animation two things have to fire at once so you have to keep consistent timing with two different properties and… well, you get it. That’s why anytime I get beyond three or four chained animations in a sequence, I usually switch to JavaScript. Personally, I like the GreenSock animation API because it has a very robust timeline functionality, but most JS animation will allow for you to easily stack animation without any recalculation which is definitely a workflow boon.

Making an animation work well is so much more than just building it. Typically, it’s the editing, refining, and debugging that take a project from merely moving to a well-orchestrated and performant piece. Hopefully these tips put a few more tools in your toolbox and smooth some rough edges in your workflow.