How to write smarter CSS animations

There are several tricks to writing efficient CSS animations that few people seem aware of. Most people borrow directly from common libraries such as Animate.css without realising how bloated they are.

You can:

  1. Omit start or end frames for greater versatility with less code.
  2. Combine similar frames.
  3. Combine animations to reduce keyframes declarations.
  4. Reverse animations to avoid separate "In" and "Out" keyframes.
  5. Use negative delays to run only portions of animations.

CodePen demos

  1. Part 1: Typical animations (what not to do).
  2. Part 2: Smarter animations (what to do).

Omit start or end frames

Most animation libraries including Animate.css define both start and end frames. This is unnecessary:

If a ‘0%’ or ‘from’ keyframe is not specified, then the user agent constructs a ‘0%’ keyframe using the computed values of the properties being animated. If a ‘100%’ or ‘to’ keyframe is not specified, then the user agent constructs a ‘100%’ keyframe using the computed values of the properties being animated.

W3C CSS Animations specification

Before

@keyframes fadeIn {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}
@keyframes fadeOut {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

After

@keyframes fadeIn {
  from {
    opacity: 0;
  }
}
@keyframes fadeOut {
  to {
    opacity: 0;
  }
}

Only define a start frame if you are animating in or an end frame if animating out. Note: It's best to only define "In" animations and reverse them when needed, see Reverse animations below.

Vendor prefixing significantly bulks out whatever you do with @keyframes so dropping a frame can mean much less CSS. More importantly, your animations will be more versatile.

An example is the easiest way to explain why. Say you have a semi-opaque image you want to animate onto the page with a fade-in effect:

img {
  opacity: 0.5;
  animation: fadeIn 0.4s;
}

Typical fadeIn keyframes animate the opacity from 0 to 1 over .4s, resulting in a blink when the the animation is over and the opacity reverts to the .5 defined.

Our better fadeIn animates the opacity from 0 intelligently to the .5 defined. No blink!

This approach can eliminate the dreaded blink for any valid animation.

Combine similar frames

Before

@keyframes flash {
  25% {
    opacity: 0;
  }
  50% {
    opacity: 1;
  }
  75% {
    opacity: 0;
  }
}

After

@keyframes flash {
  25%,
  75% {
    opacity: 0;
  }
  50% {
    opacity: 1;
  }
}

Combine animations

Instead of maintaining a long list of separate animation keyframes as found in most libraries:

  • animation-name: fadeIn
  • animation-name: slideInRight
  • animation-name: fadeInRight

Combine compatible animations instead:

  • animation-name: fadeIn
  • animation-name: slideInRight
  • animation-name: fadeIn, slideInRight

In this example we have achieved the same three animations with only two keyframes.

Note that you can only combine compatible animations. Each must be working different properties, in our example fadeIn works opacity while slideInRight works transform.

Reverse animations

If you're willing to reverse animations you can avoid defining separate "In" and "Out" animation keyframes:

@keyframes fadeIn {
  from {
    opacity: 0;
  }
}
.fade-in {
  animation: fadeIn 0.4s;
}
.fade-out {
  animation: fadeIn reverse 0.4s;
}

Negative delays

When you want to run only a portion of an existing animation use a negative animation-delay to avoid a separate keyframes declaration.

If the value for ‘animation-delay’ is a negative time offset then the animation will execute the moment it is applied, but will appear to have begun execution at the specified offset. That is, the animation will appear to begin part-way through its play cycle.

W3C CSS Animations specification

Imagine you have a zoom-in animation that looks great for small elements and a similar, less exaggerated effect for larger elements…

Before

@keyframes zoomIn {
  from {
    transform: scale(0);
  }
}
@keyframes zoomInHalf {
  from {
    transform: scale(0.5);
  }
}
.zoom-in {
  animation: zoomIn 1s;
}
.zoom-in-half {
  animation: zoomInHalf 1s linear;
}

After

@keyframes zoomIn {
  from {
    transform: scale(0);
  }
}
.zoom-in {
  animation: zoomIn 1s;
}
.zoom-in-half {
  animation: zoomIn 2s linear -1s;
}

Note:

  1. The duration has been doubled to account for it being cut in half by the negative delay.
  2. animation-timing-function influences what effect negative delays will have. linear is easiest to understand: Half the duration results in half the effect. The first half of ease has the the most action, so cutting it off results in a pretty boring animation. Play around with the duration and how much you cut off to get the effect you're after.