Chaining CSS Animations using Javascript.

CSS Animation 101 (ePub, PDF, Web) | Bypeople

TLDR;

Here is a Codepen with all the good stuff.

Animations are hard.

There are no two opinions about it; frame perfect, jitter free animations are hard by themselves. Staging animations is even harder. Fortunately, there are a number of excellent libraries that solve this very problem eg. Tachyons, however most of the times our needs are pretty simple and it seems like an overkill to use a powerful library for trivial tasks. This post attempts to outline one such approach.

Example Use case.

We have a simple Note which looks something like the picture below. On clicking “minimize” the following steps should happen, in the order they’re written.

What we’re going to animate.
  1. The note text should fade out.
  2. The content of the note should shrink until only the header is left,
  3. Note header’s colour should change from Yellow to a shade of Blue.
  4. Button text should change from ‘minimize’ to ‘maximize’.

Visually, it should look something like this:

The target animation sequence

Why not chain animations using CSS animation-delays ?

Let’s assume we have animation 1: slideInwhich is followed by animation 2 zoom. One could always add an animation-delay to zoom to make it wait. E.g

Example of Animation Delay

There are 2major problems with this.

  1. What happens when the global animation timer changes from 0.5 to 0.7 ? You guessed it right. You need to change stuff everywhere. You may be thinking that this is easy to solve using CSS Variables. This brings me to my second, and the bigger problem with this approach.
  2. What if you have painstakingly set up a chain of n animations with animation-delays which look like t 2*t 3*t ... n*t
    However, Not all animations run in the same time t , which is usually the case. E.g A Snackbar or a Toast typically stays on the screen longer than any other animated component in your page.

My point is that manually calculating when each and every animation starts and ends is simply not scalable.

Javascript to the rescue.

Let’s see how Javascript comes to our rescue here.

CSS animations are done in two ways i.e Transitions and Keyframe Animations, and Javascript provides us with two events transitionend and animationend (amongst many other related events) respectively for us to know when a certain transition or animation has ended. These are supported in all browsers. Check out Can I use for both transitionend and animationend.

In the following section we will develop a robust approach using these two events.

Talk is Cheap, Show me the Code.

Say that you need to start and animation when a transition ends. . Here is a JSFiddle Link. This is the setup.

HTML

<div class="animation-target"></div>

CSS

.animation-target {
position: relative;
left: 10px;
transition: left 2s ease-out;
}
/* Animation */@keyframes scaleIn {
to {
transform: scale(1.5);
}
}
.zoom {
animation: scaleIn 2s forwards ease-out;
}

JS

const target = document.querySelector('.animation-target');// Animation end handler
target.addEventListener('animationend', e => {
// when the animation finishes, add a text to our target
target.innerText = 'All done!';
});
// Transition end handler
target.addEventListener('transitionend', e => {
// when the transition finishes, start off the animation.
target.classList.add('zoom');
});
target.style.left = '300px'; // start off the transition.

I know what you’re thinking, the code is all over the place. It is a literal callback hell and it grows in complexity with the number of animations/transitions that need to be chained.

Fortunately, we can do much much better.

A better approach.

The problem with the above approach is that the complex callback structuring makes the code unreadable. Fortunately, we can do much better.
Can we structure the code better?

Yes. Here is one way to do it.

// common function to apply animations to an element.function animate(elem, animation) {
return new Promise((resolve, reject) => {
// Animation end handler
function handleAnimationEnd() {
console.log("animation ended...");
resolve(elem);
}
elem.addEventListener("animationend", handleAnimationEnd, { once: true });
elem.classList.add(animation);
});
}

Let’s break this down.

  1. We create a new Promise()
  2. Attach the same animationend handler that we saw earlier. Inside this handler we resolve the Promise.
  3. Apply the animation class.

This common function can be used to apply any animation, and return a new Promise which the client can work off. So how do we use this ? Pretty simple.

async function chainAnimations() {
// apply the zoom animation
await animate(target, 'zoom');
// apply another animation
await animate(target, 'scale');
// add a done text after the animation finishes.
target.style.innerText = 'something';
}

Pretty neat !

Gone are the ugly callbacks. The code is linear, flows well and is highly readable.

We can do something similar with transitions. Here goes.

function transition(elem, styleProps) {
return new Promise((resolve, reject) => {
// handler for transition end
function handleTransitionEnd() {
console.log("Transition Ended...");
resolve(elem);
}
elem.addEventListener("transitionend", handleTransitionEnd, { once: true });
// Apply all the style properties to this element
Object.entries(styleProps).forEach(([prop, value]) => {
elem.style.setProperty(prop, value);
});
});
}

The only difference here is that styleProps is a json object of style properties and values which need to be applied to our target element. Adding this to our chaining example above.

async function chainAnimations() {
// apply a transition to target
await transition(target, {
left: '200px',
top: '300px'
});
// apply the zoom animation
await animate(target, 'zoom');
// apply another animation
await animate(target, 'scale');
// add a done text after the animation finishes.
target.style.innerText = 'something';
}

Simple.

Conclusion.

Here is a Codepen putting all this together.

How is this solution better?

  1. No matter what the animation-duration is, this always works.
  2. You do not need to calculate animation-delays at all.
  3. Animations are still done using CSS, and chained using Javascript. You can plug in this solution pretty much anywhere.
  4. The callback hell is gone. The code is smooth, and flows well while being highly readable.

Finally, Great Success !

Great success

Cheers!

--

--

--

Googler. Avid Dante fan, lover of Old school Chinese Kung-fu flicks. Know a thing or two about life…

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

The Power of Memento Design Pattern in JavaScript

Clojurescript Development for JavaScript Developers in Atom with Shadow-cljs and ProtoREPL

Notes on Prototype Inheritance and Composition in JS

React Testing Library Vs. Enzyme

Server Testing Patterns for NodeJS

Model-View-Controller in JAVA

Using Modernizr with Next.JS

5 Tips To Learn Frontend Effectively

Lit sign with the number 5

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Nishant Gupta

Nishant Gupta

Googler. Avid Dante fan, lover of Old school Chinese Kung-fu flicks. Know a thing or two about life…

More from Medium

Grid system | Bootstrap 3 & 4

How to Create a Slide Transition Between Separate “Pages” with HTML, CSS, and JavaScript

Chapter 12: CSS Background

JavaScript Optional Chaining ?.