Skip to main content Accessibility Feedback

How to easily add CSS animations to your projects

For years, my go-to recommendation for CSS animations was animate.css by Daniel Eden.

It’s a great library, but it also a library. I generally only need one or two animations, and it includes way more stuff than I typically want in a project.

So I was delighted to discover Animista by Ana Travis last week. Animista is a tool that lets you select the animation you want, and then copy/paste the CSS for it into your project. That’s exactly the kind of workflow I want for stuff like this!

Today, let’s look at how to use it.

Some HTML

Let’s imagine I have a button.

<button>Click me, yo!</button>

When someone clicks it, I want the button to spin in a circle and grow bigger.

(Why? Who knows. Whimsy.)

Getting the animation CSS

On the Animsta website, I select the rotate-scale animation, and stick with the default rotate-scale-up option.

Then, I click the generate code button ({}), and unselect the autoprefixer option. Then, I copy/paste the rotation class and keyframe into my project.

.rotate-scale-up {
	animation: rotate-scale-up 0.65s linear both;
}

/* ----------------------------------------------
 * Generated by Animista on 2021-5-17 9:55:12
 * Licensed under FreeBSD License.
 * See http://animista.net/license for more info.
 * w: http://animista.net, t: @cssanimista
 * ---------------------------------------------- */

/**
 * ----------------------------------------
 * animation rotate-scale-up
 * ----------------------------------------
 */
@keyframes rotate-scale-up {
  0% {
    transform: scale(1) rotateZ(0);
  }
  50% {
    transform: scale(2) rotateZ(180deg);
  }
  100% {
    transform: scale(1) rotateZ(360deg);
  }
}

Now I’m ready to animate my button.

Adding the animation to the button

First, I use the document.querySelector() method to get my button element. Then, I add an event listener for click events on it.

// Get the button
let button = document.querySelector('button');

// Listen for clicks on the button
button.addEventListener('click', function () {
	// Do something when the button is clicked...
});

When the button is clicked, I can use the classList.add() method to add my .rotate-scale-up class to the button. This makes it rotate.

button.addEventListener('click', function () {
	button.classList.add('rotate-scale-up');
});

Here’s a demo.

Pretty cool! Unfortunately, this only works once. Let’s figure out how to fix that.

Making the animation run more than once

When the .rotate-scale-up class is added, the animation runs.

After the class is set on the element, the classList.add() method doesn’t do anything because the class already exists. With no change to the element, the animation never runs again.

To fix this, we want to listen for when the animation ends on the button, and remove the class when its done.

We can do that with the animationend event. We’ll add our listener inside the click event listener callback. We’ll also set the once option to true so that the event listener is removed after the animation.

button.addEventListener('click', function () {
	button.classList.add('rotate-scale-up');
	button.addEventListener('animationend', function () {
		button.classList.remove('rotate-scale-up');
	}, {once: true});
});

Now, the button animates every time its clicked. Here’s an updated demo.

Important accessibility concerns

Animations can make people who experience motion sickness, vertigo, and other conditions physically sick, dizzy, and disoriented.

Both Windows and macOS provide a way to disable animations at the operating system level, and tell websites that they would prefer not to see them as well.

We can access this setting through a CSS media query: prefers-reduced-motion.

@media (prefers-reduced-motion: reduce) {
	/* The user does not want animations */
}

@media (prefers-reduced-motion: no-preference) {
	/* The user is OK with animations */
}

We can use this to turn on our animations only when the user wants them, or disable them when they don’t.

For a no animations first approach, we would wrap all of our CSS animation code in prefers-reduced-motion: no-preference

.rotate-scale-up {
	animation: rotate-scale-up 0.65s linear both;
}

/* ----------------------------------------------
 * Generated by Animista on 2021-5-17 9:41:40
 * Licensed under FreeBSD License.
 * See http://animista.net/license for more info.
 * w: http://animista.net, t: @cssanimista
 * ---------------------------------------------- */

/**
 * ----------------------------------------
 * animation rotate-scale-up
 * ----------------------------------------
 */
@keyframes rotate-scale-up {
  0% {
    transform: scale(1) rotateZ(0);
  }
  50% {
    transform: scale(2) rotateZ(180deg);
  }
  100% {
    transform: scale(1) rotateZ(360deg);
  }
}

Alternatively, we can remove all animations when they’re specifically disabled.

/**
 * Remove all animations and transitions for people that prefer not to see them
 */
@media (prefers-reduced-motion: reduce) {
	* {
		animation-duration: 0.01ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.01ms !important;
		scroll-behavior: auto !important;
	}
}

We use !important here even though it’s generally frowned upon because we always want to remove animations when the user says they don’t want them.

This approach also reduces animations to a fraction of a millisecond (and thus imperceivable) rather than setting it to 0 so that any event handlers that hook into the animationend event will still run.

Here’s a demo with proper accessibility considerations.

Abstracting adding an animation

What if you have multiple animations you want to add to your project? Let’s look at how we can abstract adding and removing our animation class.

First, let’s create an animate() helper function. We’ll accept the node to animate and the animation as parameters.

function animate (node, animation) {
	// Animate stuff...
}

Inside the animate() function, we’ll add the animation class to our node, then setup the animationend event listener and remove it.

function animate (node, animation) {
	node.classList.add(animation);
	node.addEventListener('animationend', function () {
		node.classList.remove(animation);
	}, {once: true});
}

Now, we can run our animation like this.

button.addEventListener('click', function () {
	animate(button, 'rotate-scale-up');
});

If you wanted to trigger the animation in response to something else, you could run the animate() function outside of a click event.

animate(button, 'rotate-scale-up');

Here’s a demo with the abstracted code.

Adding a callback after the event

What if you wanted to do something else after the animation ends?

Let’s add one more parameter to our animate() function: a callback function to run after the animation ends. We’ll pass in the node and animation as arguments.

function animate (node, animation, onEnd = function () {}) {
	node.classList.add(animation);
	node.addEventListener('animationend', function () {
		node.classList.remove(animation);
		onEnd(node, animation);
	}, {once: true});
}

Now, you can do something like this.

animate(button, 'rotate-scale-up', function (node, animation) {
	alert(`The ${animation} animation finished on an element.`);
});

Here’s one last demo.

You can find the animate() helper function on the Vanilla JS Toolkit as well.