Staggered CSS Transitions

Avatar of Chris Coyier
Chris Coyier on

UGURUS offers elite coaching and mentorship for agency owners looking to grow. Start with the free Agency Accelerator today.

Let’s say you wanted to move an element on :hover for a fun visual effect.

@media (hover: hover) {
  .list--item {
    transition: 0.1s;
    transform: translateY(10px);
  }
  .list--item:hover,
  .list--item:focus {
    transform: translateY(0);
  }
}

Cool cool. But what if you had several list items, and you wanted them all to move on hover, but each one offset with staggered timing?

The trick lies within transition-delay and applying a slightly different delay to each item. Let’s select each list item individually and apply different delays. In this case, we’ll select an internal span just for fun.

@media (hover: hover) {
  .list li a span {
    transform: translateY(100px);
    transition: 0.2s;
  }
  .list:hover span {
    transform: translateY(0);
  }
  .list li:nth-child(1) span {
    transition-delay: 0.0s;
  }
  .list li:nth-child(2) span {
    transition-delay: 0.05s;
  }
  .list li:nth-child(3) span {
    transition-delay: 0.1s;
  }
  .list li:nth-child(4) span {
    transition-delay: 0.15s;
  }
  .list li:nth-child(5) span {
    transition-delay: 0.2s;
  }
  .list li:nth-child(6) span {
    transition-delay: 0.25s;
  }
}

See the Pen
Staggered Animations
by Chris Coyier (@chriscoyier)
on CodePen.

If you wanted to give yourself a little more programmatic control, you could set the delay as a CSS custom property:

@media (hover: hover) {
  .list {
    --delay: 0.05s;
  }
  .list li a span {
    transform: translateY(100px);
    transition: 0.2s;
  }
  .list:hover span {
    transform: translateY(0);
  }
  .list li:nth-child(1) span {
    transition-delay: calc(var(--delay) * 0);
  }
  .list li:nth-child(2) span {
    transition-delay: calc(var(--delay) * 1);
  }
  .list li:nth-child(3) span {
    transition-delay: calc(var(--delay) * 2);
  }
  .list li:nth-child(4) span {
    transition-delay: calc(var(--delay) * 3);
  }
  .list li:nth-child(5) span {
    transition-delay: calc(var(--delay) * 4);
  }
  .list li:nth-child(6) span {
    transition-delay: calc(var(--delay) * 5);
  }
}

This might be a little finicky for your taste. Say your lists starts to grow, perhaps to seven or more items. The staggering suddenly isn’t working on the new ones because this doesn’t account for that many list items.

You could pass in the delay from the HTML if you wanted:

<ul class="list">
  <li><a href="#0" style="--delay: 0.00s;">① <span>This</span></a></li>
  <li><a href="#0" style="--delay: 0.05s;">② <span>Little</span></a></li>
  <li><a href="#0" style="--delay: 0.10s;">③ <span>Piggy</span></a></li>
  <li><a href="#0" style="--delay: 0.15s;">④ <span>Went</span></a></li>
  <li><a href="#0" style="--delay: 0.20s;">⑤ <span>To</span></a></li>
  <li><a href="#0" style="--delay: 0.25s;">⑥ <span>Market</span></a></li>
</ul>
@media (hover: hover) {
  .list li a span {
    transform: translateY(100px);
    transition: 0.2s;
  }
  .list:hover span {
    transform: translateY(0);
    transition-delay: var(--delay); /* comes from HTML */
  }
}

Or if you’re Sass-inclined, you could create a loop with more items than you need at the moment (knowing the extra code will gzip away pretty efficiently):

@media (hover: hover) {
 
 /* base hover styles from above */

  @for $i from 0 through 20 {
    .list li:nth-child(#{$i + 1}) span {
      transition-delay: 0.05s * $i;
    }
  }
}

That might be useful whether or not you choose to loop for more than you need.