Chaining CSS Animations using Javascript.

Nishant Gupta
5 min readFeb 18, 2021


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


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.

A sticky note with a paragraph of text and a header
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.


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


.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;


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.
}); = '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...");
elem.addEventListener("animationend", handleAnimationEnd, { once: true });

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. = '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...");
elem.addEventListener("transitionend", handleTransitionEnd, { once: true });
// Apply all the style properties to this element
Object.entries(styleProps).forEach(([prop, value]) => {, 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. = 'something';



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!
Great success




Nishant Gupta

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