Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Luis Majano
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Luis Majano ( @lmajano )

Applying Multiple Animation Keyframes To A Loading Indicator In CSS

By on

The world seems obsessed with this idea that users don't want to see loading spinners if the loading process will only take a fraction of second. A few years ago, I demonstrated that this kind of delay can be accomplished with a simple CSS animation-delay property; but, in that post, I assumed that the loading indicator itself had no animation. That said, even if the loading indicator does have an animation, we can still use the same technique by applying multiple animation @keyframes to the same loading indicator using CSS.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

In my previous post, I used the animation-delay to keep the loading indicator at opacity:0 for a few hundred milliseconds. And then, I faded the indicator into view using opacity:1 and just left it there as a static element on the page.

In reality, my loading indicator, itself, has some sort of "pulsing" animation to it - an animation that has to repeat infinitely. As such, I can't include the opacity in that pulse @keyframes otherwise the indicator will fade in-and-out infinitely. Luckily, we can apply multiple @keyframes to a single element. And then, we can use comma-delimited sets of properties to define the behavior of each individual animation.

This means that we can have one @keyframes that defines the "fade in" animation which runs only once. And, we can use a separate @keyframes to define the "pulse" animation which will run (iterate) infinitely. Then, we can use the animation-delay on both animations to keep the loading indicator hidden briefly before it fades in and starts pulsing ad infinitum.

To see this in action, I've put together a simple jQuery demo in which I can add and remove a loading spinner to and from the DOM (Document Object Model), respectively. The spinner then uses two different animation @keyframes, one that faces the indicator in once, and one that translates it horizontally back-and-forth forever:

<!doctype html>
<html lang="en">
	<meta charset="utf-8" />
		Applying Multiple Animation Keyframes To A Loading Indicator In CSS

	<link rel="stylesheet" type="text/css" href="./demo.css" />

		Applying Multiple Animation Keyframes To A Loading Indicator In CSS

		<button class="action action--show">
			Show loading spinner
		<button class="action action--hide">
			Hide loading spinner

	<div class="ingress">
		<!-- Spinner will be injected here. -->

	<template class="template">
		<div class="spinner">

	<style type="text/css">
			The first animation is here to provide a DELAYED RENDERING of the injected
			DOM element. This allows us to inject the spinner right away, but only show
			it the user if the content takes longer than "Xms" to load.
		@keyframes spinner-fade {
			from {
				opacity: 0.0 ; /* In DOM, but visually hidden. */
			to {
				opacity: 1.0 ; /* In DOM, and visible. */
		/* The second animation is here to provide the ongoing pulsing of the spinner. */
		@keyframes spinner-pulse {
			0%, 100% {
				transform: translateX( 0px ) ;
			50% {
				transform: translateX( 10px ) ;

		.spinner {
				For our animation, we're going to attach TWO DIFFERENT animation
				keyframes using sets of comma-delimited settings. The first setting in
				each property will be applied to our FADE animation; the second setting
				in each property will be applied to our PULSE animation. We're using two
				animations because we want the first one (the fade in) to only run once
				and we want the second one (the pulse) to run infinitely.
				1, /* The FADE animation should only run once and FILL forward. */
				infinite /* The PULSE animation should repeat forever. */
				For the purposes of the demo, we're going to use a LARGE DELAY so that
				the overall effect is easier to see. This 2000ms represents the time that
				the spinner is in the DOM, but still hidden from the user.
			animation-delay: 2000ms ;   /* Same setting for both animations. */
			animation-duration: 500ms ; /* Same setting for both animations. */
			animation-fill-mode: both ; /* Same setting for both animations. */

	<script type="text/javascript" src="../../vendor/jquery/3.6.0/jquery-3.6.0.min.js"></script>
	<script type="text/javascript">

		var showButton = $( ".action--show" );
		var hideButton = $( ".action--hide" );
		var ingress = $( ".ingress" );
		var template = $( ".template" );
			function injectSpinner() {

				ingress.append( template.prop( "content" ).firstElementChild.cloneNode( true ) );

			function removeSpinner() {





As you can see, our first @keyframes uses opacity to manage the visibility of the loading indicator. And, our second @keyframes uses transform to give the loading indicator a little razzle-dazzle once it's rendered to the user. Inside of our .spinner style block, we then use a comma-delimited value for our animation-iteration property in order to make sure that the fade-in animation only runs once while the razzle-dazzle animation runs infinitely.

Now, if we run this JavaScript demo in the browser and inject the loading indicator, you can see that it delays for 2-seconds before fading in and repeating:

A loading indicator with multiple keyframes animations in CSS.

How cool is that? It works like a charm. By applying multiple CSS animation @keyframes to the same element, we get the benefit of the loading indicator while also getting - what I'm told is - a perceived performance improvement by not showing the loading indicator during a sub-second loading workflow. And, we didn't even need React Suspense!

Want to use code from this post? Check out the license.

Reader Comments

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel