Staggered Animations with CSS Custom Properties
At 4/19/2024
Movement in nature doesn’t happen all at once. Imagine a flock of birds taking off, raindrops splashing on the ground, or trees bending in the wind. The magic of these moments comes from many small movements overlapping and converging. I wanted to bring this natural movement into my web animations.
This type of animation is called staggered animation. A few years ago my colleague Tyler Sticka wrote a great article on staggering animations with JavaScript and GSAP. His article describes a really cool animation pattern he pulled off with JavaScript, but my needs were simpler, and I wanted to use CSS transitions instead of GSAP.
My Use Case
I was designing mobile navigation for a website. When a user clicked a link I wanted to slide in a list of links with a staggered animation.
The Basic Animation
First, I set up the animation without any staggering:
.Menu__link {
opacity: 0;
transform: translate(100%, -300%);
transition-duration: 0.2s;
transition-timing-function: ease-out;
transition-property: opacity, transform;
}
.Menu.is-open .Menu__link {
opacity: 1;
transform: translate(0);
}
Code language: CSS (css)
:nth-child
Adding Staggering with :nth-child
My first attempt at adding staggering used :nth-child()
and transition-delay
. This allowed me to target each link and time its transition individually:
.Menu__link:nth-of-type(2) {
transition-delay: 0.025s;
}
.Menu__link:nth-of-type(3) {
transition-delay: calc(0.025s * 2);
}
.Menu__link:nth-of-type(4) {
transition-delay: calc(0.025s * 3);
}
.Menu__link:nth-of-type(5) {
transition-delay: calc(0.025s * 4);
}
.Menu__link:nth-of-type(6) {
transition-delay: calc(0.025s * 5);
}
Code language: CSS (css)
This worked, but I had to write a lot of CSS to target each link. My CSS supported 6 links, but what if the menu grew to 7? Or 8? Would I just keep on adding :nth-child()
rules? Should I do that now to make sure this doesn’t break in the future? How many links should I support? 10? 15? 20? I figured there had to be a better way…
Custom Properties to the Rescue!
Custom properties allow you to use variables in your CSS code, and these variables can have different values in different scopes. We can even set custom properties in HTML style attributes, allowing us to write a concise CSS snippet that will work with an infinitely large list of links!
<li class="Menu__item">
<a href="#" class="Menu__link" style="--index: 0;">Babar</a>
</li>
<li class="Menu__item">
<a href="#" class="Menu__link" style="--index: 1;">Dumbo</a>
</li>
<li class="Menu__item">
<a href="#" class="Menu__link" style="--index: 2;">Echo</a>
</li>
<!-- etc. -->
Code language: HTML, XML (xml)
.Menu__link {
--index: 0;
transition-delay: calc(0.025s * var(--index));
}
Code language: CSS (css)
Reversing the Animation
When hiding the menu I wanted to reverse the animation. I could do this by removing the is-open
class, but there was a problem. The menu should animate in the opposite order when being hidden. The first link should be the first to be shown, but the last to be hidden. In order to do this I needed to swap my transition delays.
I was able to achieve this using another custom property and some calc
statements. First off, I added a new custom property that matched the number of links in my list. Since custom properties are inherited by child elements, I set this once on the list wrapper, instead of setting it on every link:
<ul style="--length: 6">
<!-- Your list items and links -->
</ul>
Code language: HTML, XML (xml)
We can then do some arithmetic to calculate different durations depending on whether we’re showing or hiding elements:
.Menu__link {
/* This delay will take effect when _hiding_ elements */
transition-delay: calc(0.025s * (var(--length) - (var(--index) + 1)));
}
.Menu.is-open .Menu__link {
/* This delay will take effect when _showing_ elements */
transition-delay: calc(0.025s * var(--index));
}
Code language: CSS (css)
With those changes in place we’ve reversed our animation when hiding elements! The first link will be the first to show, but the last to be hidden.
Dynamically Changing the Animation Order
The example above always animates in the same order. Part of what made Tyler’s previous explorations special was that the animation order varied depending on which link you clicked in the open menu. I showed an early draft of this article to Tyler and he whipped up an awesome proof of concept, showing how the two ideas could be combined to recreate a similar effect with custom properties.
Downsides and Caveats
I like staggering transitions with custom properties, but it’s important to be aware of some downsides and caveats.
Styles in HTML
For this to work we need to set these custom properties in our HTML. If we’re directly editing HTML this can get messy. That said, I’m generally not writing HTML directly.
Usually I’ll be using a templating language like Handlebars, or a framework like Vue. Here’s an example of how you could set this up in Vue:
<ul :style="`--length: ${elephants.length}`">
<li v-for="(elephant, index) in elephants" :key="elephant.id">
<a href="elephant.url" class="Menu__link" :style="`--index: ${index};`">
{{ elephant.name }}
</a>
</li>
</ul>
Code language: HTML, XML (xml)
Some developers may cringe at the lack of separation of concerns here, since we’re moving some of our styling from CSS to HTML. This solution may not work for everyone, or for every project, but in my case this tradeoff was worth it.
Browser Support
Another thing to be aware of is that older browsers like IE11 may not support custom properties. The good news is that old browsers will ignore the transition-delay
rule they don’t understand, so they’ll simply show an un-staggered transition. That said, if a large percentage of your users are still on older browsers, you may want to use the :nth-child()
option.
Accessibility
We’ll also need to properly add and remove the hidden
attribute and account for users who prefer reduced motion to make sure our animations are accessible.
Putting it All Together
By staggering our animation we’ve designed a more natural, organic feeling interaction. This is one of many ways staggered animations can be used to enhance our digital experiences. I’m excited to keep exploring and pushing the boundaries of animation on the web!