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.
I prefer the Sass version as I’ve used it for a lot of different things, I created a one color theme generator amongst other simple staggering effects on codepen. It does create a lot of nth childs, even though I haven’t taken advantage of CSS variables yet.
In production, one could argue that the CSS and HTML technique would be easier to control on a bigger scale, even though it puts the delay in a (most likely) separate document.
It doesn’t seem like it would be too difficult to implement index in the CSS specification. Nth-child but able to be used in calc. The li would presumably know how many items in the ul/ol came before it.
.list-item:nth-child(index) span {
transition-delay: calc(index + var(–delay));
}
I guess SASS is the way to go until then. or JS.
The Apple mobile site menu is a nice example of this in action. They’ve dialed it down a bit from what it used to be but you can still just about see the staggered fade effect on open/close.
Rapidly hovering and un-hovering that codepen is fun!
Today I leaned about
@media (hover: hover)
, it tests whether the device’s primary input mechanism can hover over elements. Thank you!Why does CSS-Tricks so often use list items for things that aren’t list items, like in this case? It just makes things a tiny bit harder to understand, to read and really clashes with the big picture of semantics imo.
I asked another designer why he does it and he said “Because WordPress does it.” Personally I think WordPress code is a train wreck that you should never emulate but what do I know?
It’s meant to replicate some navigation, like the old CSS-Tricks design had, and navigation should be in a list.
I don’t even know how to respond to that. WordPress, what, automatically puts all markup into lists? Where? How? What is even happening here?
I am not sure if I get the reference to WordPress correctly, but this is a usecase for a list, because it is a list of words, not a sentence intended to be read as such.
I believe what Texxs here is arguing is essentially “Navigations aren’t lists” argument, only maybe more broadly. That would make the WordPress code bit make more sense, as WordPress does default to outputting its navigation as lists.
Still… it’s an ancient battle that will never be settled. If it were a Stack Overflow question it would be closed because any answer would be ‘Primarily Opinion-based’
Using list items for your nav is very useful. CSS tricks did an article on it back in 2013, https://css-tricks.com/navigation-in-lists-to-be-or-not-to-be/
It doesn’t have anything to do with using WordPress. Lists convey navigational hierarchy with clarity. And they are good for screen readers. More info here: https://www.w3.org/TR/2008/WD-WCAG20-TECHS-20081103/H50 and in article listed above
If someone can’t explain why they use something and their only explanation is, “just because WP does it”, that’s bad.
But in this case, using lists is justifiable. The example is a list of words, the design also shows something that’s typically a menu, and that is typically a list because it is in fact a list of entries. Makes sense.
This seems like it would be a great candidate for something along the lines of:
transition-delay: calc(0.05 * attr(data-delay));
Where you’re pulling the attribute from:
<span data-delay="1">...</span>
Which… according to
calc()
we should be able to do, but I guess in the 5+ years since this was agreed upon no browser developer has actually gotten around to adding support.When I saw the use of custom properties I would have expected to pass in the index instead of the delay
<span style="--index: 1">...</span>
And then use it in the css
transition-delay: calc(var(--delay) * var(--index));
Agreed! I can’t wait for this to land.
To be fair to the browsers, the reason this doesn’t currently work isn’t due to the
calc()
spec. Theattr()
function is very limited by the CSS Level 2 spec—it can only be used in thecontent
property and the referenced value is always returned as a<string>
.With the upcoming changes in CSS Values 3, you will be able to specify
<type-or-unit>
and<fallback>
arguments toattr()
. So your example will become:Very cool and quick to digest. Thanks for sharing!
Love the thinking-out-loud in this article, Chris. It got me thinking: how easy would it be to dynamically add index CSS custom properties with just a little bit of Javascript?
Turns out it was relatively simple: I forked the pen here: https://codepen.io/kbav/pen/gOYMYJm
Next up, I want to fiddle with mutation observers to see if I could solve for dynamic list addition/subtraction/filtering/sorting/etc., since the custom properties’ order will no longer map to their “true” index.
BTW, wouldn’t it be amazing if
:nth-child(n)
selectors would allow for re-use ofn
incalc()
? :)