Skip to main content
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Ryan Anklam
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Ryan Anklam@bittersweetryan )

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!

NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.