I love little touches that make a website feel like more than just a static document. What if web content wouldn’t just “appear” when a page loaded, but instead popped, slid, faded, or spun into place? It might be a stretch to say that movements like this are always useful, though in some cases they can draw attention to certain elements, reinforce which elements are distinct from one another, or even indicate a changed state. So, they’re not totally useless, either.
So, I put together a set of CSS utilities for animating elements as they enter into view. And, yes, this pure CSS. It not only has a nice variety of animations and variations, but supports staggering those animations as well, almost like a way of creating scenes.
You know, stuff like this:
Which is really just a fancier version of this:
We’ll go over the foundation I used to create the animations first, then get into the little flourishes I added, how to stagger animations, then how to apply them to HTML elements before we also take a look at how to do all of this while respecting a user’s reduced motion preferences.
The basics
The core idea involves adding a simple CSS @keyframes
animation that’s applied to anything we want to animate on page load. Let’s make it so that an element fades in, going from opacity: 0
to opacity: 1
in a half second:
.animate {
animation-duration: 0.5s;
animation-name: animate-fade;
animation-delay: 0.5s;
animation-fill-mode: backwards;
}
@keyframes animate-fade {
0% { opacity: 0; }
100% { opacity: 1; }
}
Notice, too, that there’s an animation-delay
of a half second in there, allowing the rest of the site a little time to load first. The animation-fill-mode: backwards
is there to make sure that our initial animation state is active on page load. Without this, our animated element pops into view before we want it to.
If we’re lazy, we can call it a day and just go with this. But, CSS-Tricks readers aren’t lazy, of course, so let’s look at how we can make this sort of thing even better with a system.
Fancier animations
It’s much more fun to have a variety of animations to work with than just one or two. We don’t even need to create a bunch of new @keyframes
to make more animations. It’s simple enough to create new classes where all we change is which frames the animation uses while keeping all the timing the same.
There’s nearly an infinite number of CSS animations out there. (See animate.style for a huge collection.) CSS filters, like blur()
, brightness()
and saturate()
and of course CSS transforms can also be used to create even more variations.
But for now, let’s start with a new animation class that uses a CSS transform to make an element “pop” into place.
.animate.pop {
animation-duration: 0.5s;
animation-name: animate-pop;
animation-timing-function: cubic-bezier(.26, .53, .74, 1.48);
}
@keyframes animate-pop {
0% {
opacity: 0;
transform: scale(0.5, 0.5);
}
100% {
opacity: 1;
transform: scale(1, 1);
}
}
I threw in a little cubic-bezier()
timing curve, courtesy of Lea Verou’s indispensable cubic-bezier.com for a springy bounce.
Adding delays
We can do better! For example, we can animate elements so that they enter at different times. This creates a stagger that makes for complex-looking motion without a complex amount of code.
This animation on three page elements using a CSS filter, CSS transform, and staggered by about a tenth of a second each, feels really nice:
All we did there was create a new class for each element that spaces when the elements start animating, using animation-delay
values that are just a tenth of a second apart.
.delay-1 { animation-delay: 0.6s; }
.delay-2 { animation-delay: 0.7s; }
.delay-3 { animation-delay: 0.8s; }
Everything else is exactly the same. And remember that our base delay is 0.5s
, so these helper classes count up from there.
Respecting accessibility preferences
Let’s be good web citizens and remove our animations for users who have enabled their reduced motion preference setting:
@media screen and (prefers-reduced-motion: reduce) {
.animate { animation: none !important; }
}
This way, the animation never loads and elements enter into view like normal. It’s here, though, that is worth a reminder that “reduced” motion doesn’t always mean “remove” motion.
Applying animations to HTML elements
So far, we’ve looked at a base animation as well as a slightly fancier one that we were able to make even fancier with staggered animation delays that are contained in new classes. We also saw how we can respect user motion preferences at the same time.
Even though there are live demos that show off the concepts, we haven’t actually walked though how to apply our work to HTML. And what’s cool is that we can use this on just about any element, whether its a div, span, article, header, section, table, form… you get the idea.
Here’s what we’re going to do. We want to use our animation system on three HTML elements where each element gets three classes. We could hard-code all the animation code to the element itself, but splitting it up gives us a little animation system we can reuse.
.animate
: This is the base class that contains our core animation declaration and timing.- The animation type: We’ll use our “pop” animation from before, but we could use the one that fades in as well. This class is technically optional but is a good way to apply distinct movements.
.delay-<number>
: As we saw earlier, we can create distinct classes that are used to stagger when the animation starts on each element, making for a neat effect. This class is also optional.
So our animated elements might now look like:
<h2 class="animate pop">One!</h2>
<h2 class="animate pop delay-1">Two!</h2>
<h2 class="animate pop delay-2">Three!</h2>
Let’s count them in!
Conclusion
Check that out: we started with a seemingly basic set of @keyframes
and turned it into a full-fledged system for applying interesting animations for elements entering into view.
This is ridiculously fun, of course. But the big takeaway for me is how the examples we looked at form a complete system that can be used to create a baseline, different types of animations, staggered delays, and an approach for respecting user motion preferences. These, to me, are all the ingredients for a flexible system that’s easy to use. It gives us a lot with a little, without a bunch of extra cruft.
What we covered could indeed be a full animation library. But, of course, I didn’t stop there. I have my entire CSS file of animations in all its glory for you. There are several more types of animations in there, including 15 classes of different delays that can be used for staggering things. I’ve been using these on my own projects, but it’s still an early draft and I would love feedback on it. Please enjoy and let me know what you think in the comments!
/* ==========================================================================
Animation System by Neale Van Fleet from Rogue Amoeba
========================================================================== */
.animate {
animation-duration: 0.75s;
animation-delay: 0.5s;
animation-name: animate-fade;
animation-timing-function: cubic-bezier(.26, .53, .74, 1.48);
animation-fill-mode: backwards;
}
/* Fade In */
.animate.fade {
animation-name: animate-fade;
animation-timing-function: ease;
}
@keyframes animate-fade {
0% { opacity: 0; }
100% { opacity: 1; }
}
/* Pop In */
.animate.pop { animation-name: animate-pop; }
@keyframes animate-pop {
0% {
opacity: 0;
transform: scale(0.5, 0.5);
}
100% {
opacity: 1;
transform: scale(1, 1);
}
}
/* Blur In */
.animate.blur {
animation-name: animate-blur;
animation-timing-function: ease;
}
@keyframes animate-blur {
0% {
opacity: 0;
filter: blur(15px);
}
100% {
opacity: 1;
filter: blur(0px);
}
}
/* Glow In */
.animate.glow {
animation-name: animate-glow;
animation-timing-function: ease;
}
@keyframes animate-glow {
0% {
opacity: 0;
filter: brightness(3) saturate(3);
transform: scale(0.8, 0.8);
}
100% {
opacity: 1;
filter: brightness(1) saturate(1);
transform: scale(1, 1);
}
}
/* Grow In */
.animate.grow { animation-name: animate-grow; }
@keyframes animate-grow {
0% {
opacity: 0;
transform: scale(1, 0);
visibility: hidden;
}
100% {
opacity: 1;
transform: scale(1, 1);
}
}
/* Splat In */
.animate.splat { animation-name: animate-splat; }
@keyframes animate-splat {
0% {
opacity: 0;
transform: scale(0, 0) rotate(20deg) translate(0, -30px);
}
70% {
opacity: 1;
transform: scale(1.1, 1.1) rotate(15deg);
}
85% {
opacity: 1;
transform: scale(1.1, 1.1) rotate(15deg) translate(0, -10px);
}
100% {
opacity: 1;
transform: scale(1, 1) rotate(0) translate(0, 0);
}
}
/* Roll In */
.animate.roll { animation-name: animate-roll; }
@keyframes animate-roll {
0% {
opacity: 0;
transform: scale(0, 0) rotate(360deg);
}
100% {
opacity: 1;
transform: scale(1, 1) rotate(0deg);
}
}
/* Flip In */
.animate.flip {
animation-name: animate-flip;
transform-style: preserve-3d;
perspective: 1000px;
}
@keyframes animate-flip {
0% {
opacity: 0;
transform: rotateX(-120deg) scale(0.9, 0.9);
}
100% {
opacity: 1;
transform: rotateX(0deg) scale(1, 1);
}
}
/* Spin In */
.animate.spin {
animation-name: animate-spin;
transform-style: preserve-3d;
perspective: 1000px;
}
@keyframes animate-spin {
0% {
opacity: 0;
transform: rotateY(-120deg) scale(0.9, .9);
}
100% {
opacity: 1;
transform: rotateY(0deg) scale(1, 1);
}
}
/* Slide In */
.animate.slide { animation-name: animate-slide; }
@keyframes animate-slide {
0% {
opacity: 0;
transform: translate(0, 20px);
}
100% {
opacity: 1;
transform: translate(0, 0);
}
}
/* Drop In */
.animate.drop {
animation-name: animate-drop;
animation-timing-function: cubic-bezier(.77, .14, .91, 1.25);
}
@keyframes animate-drop {
0% {
opacity: 0;
transform: translate(0,-300px) scale(0.9, 1.1);
}
95% {
opacity: 1;
transform: translate(0, 0) scale(0.9, 1.1);
}
96% {
opacity: 1;
transform: translate(10px, 0) scale(1.2, 0.9);
}
97% {
opacity: 1;
transform: translate(-10px, 0) scale(1.2, 0.9);
}
98% {
opacity: 1;
transform: translate(5px, 0) scale(1.1, 0.9);
}
99% {
opacity: 1;
transform: translate(-5px, 0) scale(1.1, 0.9);
}
100% {
opacity: 1;
transform: translate(0, 0) scale(1, 1);
}
}
/* Animation Delays */
.delay-1 {
animation-delay: 0.6s;
}
.delay-2 {
animation-delay: 0.7s;
}
.delay-3 {
animation-delay: 0.8s;
}
.delay-4 {
animation-delay: 0.9s;
}
.delay-5 {
animation-delay: 1s;
}
.delay-6 {
animation-delay: 1.1s;
}
.delay-7 {
animation-delay: 1.2s;
}
.delay-8 {
animation-delay: 1.3s;
}
.delay-9 {
animation-delay: 1.4s;
}
.delay-10 {
animation-delay: 1.5s;
}
.delay-11 {
animation-delay: 1.6s;
}
.delay-12 {
animation-delay: 1.7s;
}
.delay-13 {
animation-delay: 1.8s;
}
.delay-14 {
animation-delay: 1.9s;
}
.delay-15 {
animation-delay: 2s;
}
@media screen and (prefers-reduced-motion: reduce) {
.animate {
animation: none !important;
}
}
“Everything else is exactly the same. And remember that our base delay is 0.5s, so these helper classes count up from there.”
Maybe I am missing something here, but I don’t see this as being the case in your example. The 0.6s, 0.7s and 0.8s is overriding the 0.5s that was defined. The way it’s worded in your explanation I took it to mean the delay would be 0.5s + 0.6s, 0.5s + 0.7s, 0.5s + 0.8s.
Pretty sure it’s intended to imply:
‘the default delay is .5s so we only need to create helper functions counting up from there”
Absolutely amazing
I like the way you classified delays and that animation library & ease calculator is new to me.
It maybe kinda cheating but I really like Animista (I’d rather ppl search than link drop, but .net for clarity, not advertising).
You can visualize and tweak every part, even gratients then copy paste the CSS to anywhere. It used siliar naming conventions.
So it doesn’t do everything for you, but it’s the best animation editor that I’ve used.
Nice. I created a similar (but not as extensive) little helper a while ago.
One thing I never got to, but could be worth exploring for your project, is how you could make use of CSS variables to make the delay class dynamic and therefore not rely on a numbered class system (eg, having to account for the possiblity of “.delay-18”).
For example, looping through generated items on a product screen where you may have 20 products being loaded, each item would pass their index value to the CSS and it would do the math to delay it’s animation.
You could also possibly do something similar using nth child where the CSS figures out where the element is in the order of things.
Good luck!
If it’s generated data, you could potentially put the total number of elements as a CSS variable inside the parent and then use that to dynamically adjust the delay.
I love this idea, I just haven’t figured out the ideal way to implement it. I’m open to ideas on how to do it.
I generate a container with
n
children (the number of children set as a custom property on the container and each child having the index as a custom property as well) using Pug:This produces the following HTML:
And then for styling, I normally set an animation duration
t
as a SCSS variable (if there’s no need for it to be dynamic, it’s better for perf not to use a CSS variable) and the base delay tot/n
and then multiply that base delay with the item indexi
for the actual item delay. Though the base delay could be anything else.Something else I would do in this case is ditch all those
100%
keyframes, they’re completely unnecessary. Those are the property values in the case of no animation, the browser knows to animate to those if they’re not explicitly written.And in the
animate-drop
case,opacity: 1
is only necessary in the95%
keyframe. I’d also use a single value for uniform scaling because it’s terribly confusing to see stuff likescale(1.1, 1.1)
– I looked at that about five times trying to figure out the difference between those two values. If I see two values insidescale()
, I expect them to be different.A bit late reply but we do exactly this in animxyz.com where we take all the boilerplate of creating animations like this and let you compose them easily with some utility-class like attributes and CSS variables.
Using an index CSS variable we handle an arbitrary number of elements with staggered delays, even reversed delays if needed.
FWIW, I used this for a generated photo gallery.
Instead of creating a bunch of delay-X classes, I just set the delay as a style attribute on the element.
Before the loop, set the counter to the base delay (0.5) and then each iteration increments by 0.1. As long as you’re generating them in the order you want to display them, this works well.
Ana – Thanks for these comments. I will update the code when I get the chance.
Jill – Awesome to see this being used for something. Thank you!
Would these neat animation affect the web vital metric in a bad way?
It’d need some investigation. It’s possible.
Nice
This is exactly how I created css animations in max WordPress theme back in 2013. Added the load class when the element came into the actual “view” of page
Cool stuff! I think it could benefit from some variables that hold certain repetitive values, as well as some parametrics that are constant.
For example, the cubic-bezier() itself could be served with vars just like a composable hsl(), but to a higher level of abstraction even hold a variable that just stores the ‘–timing-function-type: cubic-bezier(); to swap out timings. Overkill, but still.
The animation delay utilities at the end should have an incremental value that applies calc() for more flexibility, but I don’t know performance-wise if that matters. But maintenance to update and adjust on a component level would be greatly improved, such is the case with design systems imo; they serve DX first, not UX first.
That’s the outcome the end-user benefits from, the process of setting it up is what “we” need to benefit from.
Lastly, prefers reduce motion is something that doesn’t mean “no motion at all”, so what’s your take on supporting the feature if certain UI messages might need them to convey message & meaning?
Maybe I’m digging too deep, but I like a thoroughly humble & hardcore system like this. I used it for reference for my own DS, with some of this feedback applied in due time.
Thanks for sharing!
Thank you for this comment.
I had not thought of using a variable within the cubic-bezier, but it’s potentially pretty brilliant. You could think of the third value as the deceleration (sort of) and the fourth value as how much it’ll overshoot.
As for using calc for the delays, I actually had a variation of that originally! It’s great, but I pulled it to simplify the article. It would be a great thing to add back in, likely as a multiplier variable added to the delay classes. I’ll probably add this at some point to my production version.
Very true about reduced motion. I had considered just turning all animations into fades with zero delays when reduce motion is on. That could be a better approach than no animations at all.
I’ve liked putting using a
calc()
with custom-properties placed in the element to assign the fade-in order: