Staggered Animations with CSS Custom Properties

Written by Paul Hebert on

A series of arrows with staggered positions and opacity, over a colored background.

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__link {
  opacity: 1;
  transform: translate(0);

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);

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 class="Menu__item">
  <a href="#" class="Menu__link" style="--index: 1;">Dumbo</a>
<li class="Menu__item">
  <a href="#" class="Menu__link" style="--index: 2;">Echo</a>
<!-- etc. -->
.Menu__link {
  --index: 0;
  transition-delay: calc(0.025s * var(--index));

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 -->

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__link {
  /* This delay will take effect when _showing_ elements */
  transition-delay: calc(0.025s * var(--index));

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="">
    <a href="elephant.url" class="Menu__link" :style="`--index: ${index};`">
      {{ }}

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.


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!

Paul Hebert

Paul Hebert is a hybrid designer and developer at Cloud Four. When he's not designing and developing websites he enjoys bouldering, drawing, cooking, gardening, and eating too much cheese.

Never miss an article!

Get Weekly Digests


You can use custom property for management open state:

.Menu {
--is-open: 0;
/* ... */
} {
--is-open: 1;

and then you get declarative description:

.Menu__link {
opacity: var(--is-open);
transform: translateX(calc((1 - var(--is-open)) * 100%));
transition-delay: calc(0.025s * (var(--index) * (2 * var(--is-open) - 1) + (var(--length ) + 1) * (1 - var(--is-open))));

demo –

Let’s discuss your project! Email Us