Animating elements, at its most basic, is fairly straightforward. Define the keyframes. Name the animation. Call it on an element.
But sometimes we need something a little more complex to get the right “feel” for the way things move. For example, a sound equalizer might use the same animation on each bar, but they are staggered to give the illusion of being animated independently.
See the Pen
Apple Music Sound Equilizer in SVG by Geoff Graham (@geoffgraham)
on CodePen.
I was recently building a dashboard and wanted the items in one of the widgets to flow into view with a staggered animation.
Just like the sound equalizer above, I started going down the :nth-child
route. I used the unordered list (<ul>
) as the parent container, gave it a class and employed the :nth-child
pseudo selector to offset each list item with animaton-delay
.
.my-list li {
animation: my-animation 300ms ease-out;
}
.my-list li:nth-child(1) {
animation-delay: 100ms;
}
.my-list li:nth-child(2) {
animation-delay: 200ms;
}
.my-list li:nth-child(3) {
animation-delay: 300ms;
}
/* and so on */
This technique does indeed stagger items well, particularly if you know how many items are going to be in the list at any given time. Where things fall apart, however, is when the number of items is unpredictable, which was the case for the widget I was building for the dashboard. I really didn’t want to come back to this piece of code every time the number of items in the list changed, so I knocked out a quick Sass loop that accounts for up to 50 items and increments the animation delay with each item:
.my-list {
li {
animation: my-animation 300ms ease-out;
@for $i from 1 through 50 {
&:nth-child(#{$i}) {
animation-delay: 100ms * $i;
}
}
}
}
That should do it! Yet, it feels way too hacky. Sure, it doesn’t add that much weight to the file, but you know the compiled CSS will include a bunch of unused selectors, like nth-child(45)
.
There must be a better way. This is where I would normally reach for JavaScript to find all of the items and add a delay but… this time I spent a little time exploring to see if there is a way to do it with CSS alone.
How about CSS counters?
The first thing I thought of was using a CSS counter in combination with the calc()
function:
.my-list {
counter-reset: my-counter;
}
.my-list li {
counter-increment: my-counter;
animation-delay: calc(counter(my-counter) * 100ms);
}
Unfortunately, that won’t work because the spec says counters cannot be used in calc()
):
Components of a
calc()
expression can be literal values orattr()
orcalc()
expressions.
Turns out a few people like this idea, but it hasn’t gone further than the draft stage.
How about a data attribute?
Having read that excerpt from the spec, I learned that calc()
can use attr()
. And, according to the CSS Values and Units specification):
In CSS3, the
attr()
expression can return many different types
This made me think; perhaps a data attribute could do the trick.
<ul class="my-list">
<li data-count="1"></li>
<li data-count="2"></li>
<li data-count="3"></li>
<li data-count="4"></li>
</ul>
.my-list li {
animation-delay: calc(attr(data-count) * 150ms);
}
But my hopes were dashed as the browser support for this is diabolical!
This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.
Desktop
Chrome | Firefox | IE | Edge | Safari |
---|---|---|---|---|
No | No | No | No | No |
Mobile / Tablet
Android Chrome | Android Firefox | Android | iOS Safari |
---|---|---|---|
No | No | No | No |
So, back to the drawing board.
How about custom properties?
The next idea I had was using CSS custom properties. It’s not pretty, but it worked 🙂
See the Pen
CSS variables animation order by Dan Benmore (@dbenmore)
on CodePen.
Turns out it’s pretty flexible too. For example, the animation can be reversed:
See the Pen
CSS variables reverse animation order by Dan Benmore (@dbenmore)
on CodePen.
It can also do something completely random and animate elements at the same time:
See the Pen
CSS variables random animation order by Dan Benmore (@dbenmore)
on CodePen.
We can even push it a bit further and do a diagonal swoosh:
See the Pen
Set animation stagger with CSS properties / variables by Dan Benmore (@dbenmore)
on CodePen.
The browser support isn’t all that bad (pokes stick at Internet Explorer).
This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.
Desktop
Chrome | Firefox | IE | Edge | Safari |
---|---|---|---|---|
49 | 31 | No | 16 | 10 |
Mobile / Tablet
Android Chrome | Android Firefox | Android | iOS Safari |
---|---|---|---|
119 | 119 | 119 | 10.0-10.2 |
One of the great features of CSS is that it will ignore things it doesn’t understand, thanks to the cascade. That means everything will animate in into view together. If that’s not your bag, you can add a feature query to override a default animation:
.my-list li {
animation: fallback-animation;
}
@supports (--variables) {
.my-list li {
animation: fancy-animation;
animation-delay: calc(var(--animation-order) * 100ms);
}
}
Vanilla CSS FTW
The more I stop and ask myself whether I need JavaScript, the more I’m amazed what CSS can do on its own. Sure, it would be nice if CSS counters could be used in a calc()
function and it would be a pretty elegant solution. But for now, inline custom properties provide both a powerful and flexible way to solve this problem.
If you’re using some sort of templating language an easy way to do is inlining a transition-delay property on the style attribute, like so:
{% for i in 0..10 %}
{% endfor %}
I’d still take setting an index variable in the loop over setting a
transition-delay
or another such property. This is because the index variable can come in handy for setting more than one property. For example, this index can also control the hue for thebackground
or a horizontal offset.It can also control the
transition-duration
if we want it to be different, or multiple durations and delays if we want to animate multiple properties.I literally just applied this to a website last week using the Sass loop. Now I’m going to try the Custom Properties approach. Thanks for putting this article together!
Nice! Staggered delays are something that I’ve liked more in SASS/SCSS than styled-components. Combining this idea with mapped indexes in React is gonna make this ish super easy. You The MAN!
I’m building everything in react anyways, so I just use the index to add an animation delay multiplier. Your method still requires you to write a variable for each element which won’t work with dynamic lists.
The Sass Loop is a fantastic idea. There was no hope for me to do such things without js. Many Thanks. I will pay more attention to css in the future.
I just go the JS route:
Great article. One issue with staggered animations, particularly with delay is that ad heavy pages with a busy main thread, often ruin the effect (IOS is worse).
You may still need to animate only when the thread is free
Great article, and ideas