Applying Multiple Animation Keyframes To A Loading Indicator In CSS
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">
<head>
<meta charset="utf-8" />
<title>
Applying Multiple Animation Keyframes To A Loading Indicator In CSS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css" />
</head>
<body>
<h1>
Applying Multiple Animation Keyframes To A Loading Indicator In CSS
</h1>
<p>
<button class="action action--show">
Show loading spinner
</button>
<button class="action action--hide">
Hide loading spinner
</button>
</p>
<div class="ingress">
<!-- Spinner will be injected here. -->
</div>
<template class="template">
<div class="spinner">
Loading....
</div>
</template>
<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.
*/
animation-name:
spinner-fade,
spinner-pulse
;
animation-iteration-count:
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. */
}
</style>
<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" );
showButton.click(
function injectSpinner() {
ingress.append( template.prop( "content" ).firstElementChild.cloneNode( true ) );
}
);
hideButton.click(
function removeSpinner() {
ingress.empty();
}
);
</script>
</body>
</html>
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:
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