Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Fri, 07 Nov 2025 15:08:19 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.3 225069128 Staggered Animation with CSS sibling-* Functions https://frontendmasters.com/blog/staggered-animation-with-css-sibling-functions/ https://frontendmasters.com/blog/staggered-animation-with-css-sibling-functions/#respond Fri, 07 Nov 2025 15:08:18 +0000 https://frontendmasters.com/blog/?p=7631 The CSS functions sibling-index() and sibling-count() return an element’s position relative to its siblings and the total number of siblings, including itself. This is useful for styling elements based on their positions.

<div class="parent"> <!-- sibling-count() = 3 -->
  <div class="child"></div> <!-- sibling-index() = 1 -->
  <div class="child"></div> <!-- sibling-index() = 2 -->
  <div class="child"></div> <!-- sibling-index() = 3 -->
</div>

For instance, to create a pyramid chart, we could proportionally increase the widths of elements as they align.

The integers returned by sibling-index() and sibling-count() can be easily computed with other data types like length, angle, and time. An incremental or decremental time sequence is the foundation of any staggered animation where elements animate consecutively. For example:

.el {
  animation-delay: calc(sibling-index() * 0.1s);
}

This post covers a demo where selecting an item causes the preceding and succeeding items to disappear sequentially from the outside.

The Layout

<main class="cards-wrapper">
  <div class="cards"><input type="checkbox" aria-label="movie, only yesterday"></div>
  <div class="cards"><input type="checkbox" aria-label="movie, the wind rises"></div>
  <div class="cards"><input type="checkbox" aria-label="movie, howl's moving castle"></div>
  <div class="cards"><input type="checkbox" aria-label="movie, ponyo"></div>
  <div class="cards"><input type="checkbox" aria-label="movie, the cat returns"></div>
</main>
.cards-wrapper {
  display: flex;
  gap: 10px;
  justify-content: center;
}

.cards {
  width: 15vw;
  aspect-ratio: 3/4.2;
  contain: layout;
  /* etc. */

  input[type="checkbox"] {
    position: absolute;
    inset: 0;
  }
}

This HTML contains a group of .cards inside a .cards-wrapper, arranged horizontally using flexbox. Each card has a checkbox covering its entire size, triggering selection on click.

The Selectors

Before seeing what happens when a card is selected, let’s see the different CSS selectors we’ll be using to target the different parts of the user interface.

.cards:has(:checked) {

  /* Style rules for the chosen card (the one with a checked box).
    Highlight the chosen card. */   

  .cards-wrapper:has(:checked) .cards:not(&) {
    /* Style rules for the remaining cards when one is chosen.  
      Animate the unchosen cards to disappear. */ 
  }

  & ~ .cards {
    /* Style rules for cards to the right-side of the chosen one. 
       Set a decremental delay time for the disappearance. */

    .cards:not(&) {
       /* Style rules for cards that aren't to the right of the chosen one. 
          Set an incremental delay time. */
    }
  } 
}

The Declarations

Highlight The Chosen Card

The selected card gets a grey dashed border:

border: 2px dashed #888;

Animate The Unchosen Cards to Disappear

opacity: 0;
width: 0; 
display: none; 
transition: 0.3s calc(var(--n) * 0.2s) all;
transition-behavior: allow-discrete;

input { 
  pointer-events: none; 
}
  1. Zero opacity and width create a fade out and horizontal shrinking of the card, including the space the card occupies.
  2. transition-behavior: allow-discrete allows display: none to apply at the end of the transition, which is appropriate as we do want to apply that to remove them from the accessibility tree.
  3. The transition time, affecting all properties that change, is 0.3s. The transition delay is a multiple of 0.2s and the CSS variable --n (to be discussed)
  4. To prevent unchosen cards from being clicked and chosen while disappearing, their checkbox input get pointer-events: none

Decremental Delay Time for Cards On The Right

The cards, succeeding the chosen one, need descending transition delay times. Thus, --n is decremented for each element (left to right) to the right of the chosen card.

--n: calc(sibling-count() - sibling-index() + 1);

Let’s say the third card is chosen. The remaining cards (4 to last) have to vanish in reverse order. The math below shows each of those cards’ --n value and delay time (--n × 0.2s).

Our example has five cards, so sibling-count() is 5. Let’s see how --n calculated for each card after the 3rd (chosen) card:

4th card
--------
--n = calc( sibling-count() - sibling-index() + 1 )
--n = calc( 5 - 4 + 1 )
--n = 2

delay time = calc( var(--n) * 0.2s )
delay time = calc( 2 * 0.2s )
delay time = 0.4s


5th card
--------
--n = calc( sibling-count() - sibling-index() + 1 )
--n = calc( 5 - 5 + 1 )
--n = 1

delay time = calc( var(--n) * 0.2s )
delay time = calc( 1 * 0.2s )
delay time = 0.2s

Incremental Delay Time for Cards On The Left

The sibling-index() value alone is enough, since the delay time of the elements (left to right) to the left of the chosen card increases outward in. First card goes first, then the second, and so forth.

 --n: sibling-index();

Here’s now --n calculates and thus makes the delay time for each card before the 3rd chosen card:

1st card
--------
--n = sibling-index()
--n = 1

delay time = calc( var(--n) * 0.2s )
delay time = calc( 1 * 0.2s )
delay time = 0.2s


2nd card
--------
--n = sibling-index()
--n = 2

delay time = calc( var(--n) * 0.2s )
delay time = calc( 2 * 0.2s )
delay time = 0.4s

The Rulesets

All combined, these are the style rules that are applied when a card is chosen:

/* chosen card */
.cards:has(:checked) {

   border: 2px dashed #888;

  /* cards that aren't chosen, when one has been */
  .cards-wrapper:has(:checked) .cards:not(&) {
    opacity: 0;
    width: 0; 
    display: none; 
    transition: .3s calc(var(--n) * .2s) all;
    transition-behavior: allow-discrete;
    input { pointer-events: none; }
  }

  /* cards after the chosen one */
  & ~ .cards{
    --n: calc(sibling-count() - sibling-index() + 1);
    /* cards not after the chosen one */
    .cards:not(&){
      --n: sibling-index();
    }
  } 
}

Note: Among the “cards not after the chosen one” (see above snippet) the chosen card is also included, but since it doesn’t have the transition declaration, there’s no effect. If you want, you can exclude it by adding :not(:has(:checked)) to the selector, but it’s not necessary in our example.

The Fallback

If a browser doesn’t support sibling-* functions, we can calculate --n in JavaScript to determine the elements’ positions among their siblings.

if(!CSS.supports('order', 'sibling-index()')) {

  // Turn the NodeList to an array for easier manipulation
  const CARDS = Array.from(document.querySelectorAll('.cards'));

  document.querySelector('.cards-wrapper').addEventListener('change', (e) => {

    // Index of the card with the checkbox that fired the 'change' event.
    const IDX = CARDS.indexOf(e.target.parentElement);

    // All cards after the chosen one. If there are three,
    // they get --n values of 3, 2, 1
    CARDS.slice(IDX + 1).forEach((card, idx, arr) 
      => card.style.setProperty('--n', `${arr.length - idx}`));

    // All cards up to the chosen one. If there are three,
    // they get --n values of 1, 2, 3
    CARDS.slice(0, IDX).forEach((card, idx) 
       => card.style.setProperty('--n', `${idx + 1}`));
  });
}

Below is another example, where you’ll be able see an accordion sort of animation by using the sibling-index() function to show and hide the items, even without transition delay, but with the delay the staggered effect comes through.

]]>
https://frontendmasters.com/blog/staggered-animation-with-css-sibling-functions/feed/ 0 7631
Infinite Marquee Animation using Modern CSS https://frontendmasters.com/blog/infinite-marquee-animation-using-modern-css/ https://frontendmasters.com/blog/infinite-marquee-animation-using-modern-css/#comments Mon, 04 Aug 2025 18:30:28 +0000 https://frontendmasters.com/blog/?p=6673 A set of logos with an infinite repeating slide animation is a classic component in web development. We can find countless examples and implementations starting from the old (and now deprecated) <marquee> element. I’ve written an article about it myself a few years ago.

“Why another article?” you ask. CSS keeps evolving with new and powerful features, so I always try to find room for improvement and optimization. We’ll do that now with some new CSS features.

At the time of writing, only Chrome-based browsers have the full support of the features we will be using, which include features like shape(), sibling-index(), and sibling-count().

In the demo above, we have an infinite marquee animation that works with any number of images. Simply add as many elements as you want in the HTML. There is no need to touch the CSS. You can easily control the number of visible images by adjusting one variable, and it’s responsive. Resize the screen and see how things adjust smoothly.

You might think the code is lengthy and full of complex calculations, but it’s less than 10 lines of CSS with no JavaScript.

.container {
  --s: 150px; /* size of the images */
  --d: 8s; /* animation duration */
  --n: 4; /* number of visible images */
  
  display: flex;
  overflow: hidden;
}
img {
  width: var(--s);
  offset: shape(from calc(var(--s)/-2) 50%,hline by calc(sibling-count()*max(100%/var(--n),var(--s))));
  animation: x var(--d) linear infinite calc(-1*sibling-index()*var(--d)/sibling-count());
}
@keyframes x { 
  to { offset-distance: 100%; }
}

Perhaps this looks complex at first glance, especially that strange offset property! Don’t stare too much at it; we will dissect it together, and by the end of the article, it will look quite easy.

The Idea

The tricky part when creating a marquee is to have that cyclic animation where each element needs to “jump” to the beginning to slide again. Earlier implementations will duplicate the elements to simulate the infinite animation, but that’s not a good approach as it requires you to manipulate the HTML, and you may have accessibility/performance issues.

Some modern implementations rely on a complex translate animation to create the “jump” of the element outside the visible area (the user doesn’t see it) while having a continuous movement inside the visible area. This approach is perfect but requires some complex calculation and may depend on the number of elements you have in your HTML.

It would be perfect if we could have a native way to create a continuous animation with the “jump” and, at the same time, make it work with any number of elements. The first part is doable and we don’t need modern CSS for it. We can use offset combined with path() where the path will be a straight line.

Inside path, I am using the SVG syntax to define a line, and I simply move the image along that line by animating offset-distance between 0% and 100%. This looks perfect at first glance since we have the animation we want but it’s not a flexible approach because path() accepts only hard-coded pixel values.

To overcome the limitation of path(), we are going to use the new shape() function! Here is a quote from the specification:

The shape() function uses a set of commands roughly equivalent to the ones used by path(), but does so with more standard CSS syntax, and allows the full range of CSS functionality, such as additional units and math functions … In that sense, shape() is a superset of path().

Instead of drawing a line using path(), we are going to use shape() to have the ability to rely on CSS and control the line based on the number of elements.

Here is the previous demo using shape():

If you are unfamiliar with shape(), don’t worry. Our use case is pretty basic as we are going to simply draw a horizontal line using the following syntax:

offset: shape(from X Y, hline by length);

The goal is to find the X Y values (the coordinates of the starting point) and the length value (the length of the line).

The Implementation

Let’s start with the HTML structure, which is a set of images inside a container:

<div class="container">
  <img src="">
  <img src="">
  <!-- as many images as you want -->
</div>

We make the container flexbox to remove the default space between the image and make sure they don’t wrap even if the container is smaller (remember that flex-wrap is by default nowrap).

Now, let’s suppose we want to see only N images at a time. For this, we need to define the width of the container to be equal to N x size_of_image.

.container {
  --s: 100px; /* size of the image */
  --n: 4; /* number of visible images */

  display: flex;
  width: calc(var(--n) * var(--s));
  overflow: hidden;
}
img {
  width: var(--s);
}

Nothing complex so far. We introduced some variables to control the size and the number of visible images. Now let’s move to the animation.

To have a continuous animation, the length of the line needs to be equal to the total number of images multiplied by the size of one image. In other words, we should have a line that can contain all the images side by side. The offset property is defined on the image elements, and thanks to modern CSS, we can rely on the new sibling-count() to get the total number of images.

offset: shape(from X Y, hline by calc(sibling-count() * var(--s)));

What about the X Y values? Let’s try 0 0 and see what happens:

Hmm, not quite good. All the images are above each other, and their position is a bit off. The first issue is logical since they share the same animation. We will fix it later by introducing a delay.

The trickiest part when working with offset is defining the position. The property is applied on the child elements (the images in our case), but the reference is the parent container. By specifying 0 0, we are considering the top-left corner of the parent as the starting point of the line.

What about the images? How are they placed? If you remove the animation and keep the offset-distance equal to 0% (the default value), you will see the following.

An animated marquee with text that moves horizontally across a container, showcasing a modern CSS implementation for infinite scrolling images or text.

The center of the images is placed at the 0 0, and starting from there, they move horizontally until the end of the line. Let’s update the X Y values to rectify the position of the line and bring the images inside the container. For this, the line needs to be in the middle 0 50%.

offset: shape(from 0 50%, hline by calc(sibling-count() * var(--s)));

It’s better, and we can already see the continuous animation. It’s still not perfect because we can see the “jump” of the image on the left. We need to update the position of the line so it starts outside the container and we don’t see the “jump” of the images. The X value should be equal to -S/2 instead of 0.

offset: shape(from calc(var(--s)/-2) 50%, hline by calc(sibling-count() * var(--s)));

No more visible jump, the animation is perfect!

To fix the overlap between the images, we need to consider a different delay for each image. We can use nth-child() to select each image individually and define the delay following the logic below:

img:nth-child(1) {animation-delay: -1 *  duration/total_image }
img:nth-child(2) {animation-delay: -2 *  duration/total_image }
/* and so on */

Tedious work, right? And we need as many selectors as the number of images in the HTML code, which is not good. What we want is a generic CSS code that doesn’t depend on the HTML structure (the number of images).

Similar to the sibling-count()that gives us the total number of images, we also have sibling-index() that gives us the index of each image within the container. All we have to do is to update the animation property and include the delay using the index value that will be different for each image, hence a different delay for each image!

animation: 
  x var(--d) linear infinite 
  calc(-1*sibling-index()*var(--d)/sibling-count());

Everything is perfect! The final code is as follows:

.container {
  --s: 100px; /* size of the image */
  --d: 4s; /* animation duration */
  --n: 4; /* number of visible images */
  
  display: flex;
  width: calc(var(--n) * var(--s));
  overflow: hidden;
}
img {
  width: var(--s);
  offset: shape(from calc(var(--s)/-2) 50%, hline by calc(sibling-count() * var(--s)));
  animation: x var(--d) linear infinite calc(-1*sibling-index()*var(--d)/sibling-count());
}
@keyframes x { 
  to {offset-distance: 100%}
}

We barely have 10 lines of CSS with no hardcoded values or magic numbers!

Let’s Make it Responsive

In the previous example, we fixed the width of the container to accommodate the number of images we want to show but what about a responsive behavior where the container width is unknown? We want to show only N images at a time within a container that doesn’t have a fixed width.

The observation we can make is that if the container width is bigger than NxS, we will have space between images, which means that the line defined by shape() needs to be longer as it should contain the extra space. The goal is to find the new length of the line.

Having N images visible at a time means that we can express the width of the container as follows:

width = N x (image_size + space_around_image)

We know the size of the image and N (Defined by --s and --n), so the space will depend on the container width. The bigger the container is, the more space we have. That space needs to be included in the length of the line.

Instead of:

hline by calc(sibling-count() * var(--s))

We need to use:

hline by calc(sibling-count() * (var(--s) + space_around_image))

We use the formula of the container width and replace (var(--s) + space_around_image) with width / var(--n) and get the following:

hline by calc(sibling-count() * width / var(--n) )

Hmm, what about the width value? It’s unknown, so how do we find it?

The width is nothing but 100%! Remember that offset considers the parent container as the reference for its calculation so 100% is relative to the parent dimension. We are drawing a horizontal line thus 100% will resolve to the container width.

The new offset value will be equal to:

shape(from calc(var(--s)/-2) 50%, hline by calc(sibling-count() * 100% / var(--n)));

And our animation is now responsive.

Resize the container (or the screen) in the below demo and see the magic in play:

We have the responsive part but it’s still not perfect because if the container is too small, the images will overlap each other.

We can fix this by combining the new code with the previous one. The idea is to make sure the length of the line is at least equal to the total number of images multiplied by the size of one image. Remember, it’s the length that allows all the images to be contained within the line without overlap.

So we update the following part:

calc(sibling-count() * 100%/var(--n))

With:

max(sibling-count() * 100%/var(--n), sibling-count() * var(--s))

The first argument of max() is the responsive length, and the second one is the fixed length. If the first value is smaller than the second, we will use the latter and the images will not overlap.

We can still optimize the code a little as follows:

calc(sibling-count() * max(100%/var(--n),var(--s)))

We can also add a small amount to the fixed length that will play the role of the minimum gap between images and prevent them from touching each other:

calc(sibling-count() * max(100%/var(--n),var(--s) + 10px))

We are done! A fully responsive marquee animation using modern CSS.

Here is again the demo I shared at the beginning of the article with all the adjustments made:

Do you still see the code as a complex one? I hope not!

The use of min() or max() is not always intuitive, but I have a small tutorial that can help you identify which one to use.

More Examples

I used images to explain the technique, but we can easily extend it to any kind of content. The only requirement/limitation is to have equal-width items.

We can have some text animations:

Or more complex elements with image + text:

In both examples, I am using flex-shrink: 0 to avoid the default shrinking effect of the flex items when the container gets smaller. We didn’t have this issue with images as they won’t shrink past their defined size.

Conclusion

Some of you will probably never need a marquee animation, but it was a good opportunity to explore modern features that can be useful such as the shape() and the sibling-*() functions. Not to mention the use of CSS variables, calc(), max(), etc., which I still consider part of modern CSS even if they are more common.

]]>
https://frontendmasters.com/blog/infinite-marquee-animation-using-modern-css/feed/ 6 6673
View Transitions Staggering https://frontendmasters.com/blog/view-transitions-staggering/ https://frontendmasters.com/blog/view-transitions-staggering/#comments Tue, 22 Oct 2024 16:42:53 +0000 https://frontendmasters.com/blog/?p=4232 I love view transitions. When you’re using view transitions to move multiple items, I think staggering them is cool effect and a reasonable ask for doing so succinctly. While I was playing with this recently I learned a lot and a number of different related tech and syntax came up, so I thought I’d document it. Blogging y’all, it’s cool. You should.

Example

So let’s say we have a menu kinda thing that can open & close. It’s just an example, feel free to use your imagination to consider two states of any UI with multiple elements. Here’s ours:

Closed
Open

View Transitions is a great way to handle animating this menu open. I won’t beat around the bush with a working example. Here’s that:

That works in all browsers (see support). It animates (with staggering) in Chrome and Safari, and at this time of this writing, just instantly opens and closes in Firefox (which is fine, just less fancy).

Unique View Transition Names

In order to make the view transition work at all, every single item needs a unique view-transition-name. Otherwise the items will not animate on their own. If you ever seen a view transition that has a simple fade-out-fade-in, when you were trying to see movement, it’s probably a problem with unique view-transition-names.

This brings me to my first point. Generating unique view-transition-names is a bit cumbersome. In a “real world” application, it’s probably not that big of a deal as you’ll likely be using some kind of templating that could add it. Some variation of this:

<div class="card"
     style="view-transition-name: card-<%= card.id %>">

<!-- turns into -->

<div class="card" 
     style="view-transition-name: card-987adf87aodfasd;">

But… you don’t always have access to something like that, and even when you do, isn’t it a bit weird that the only real practical way to apply these is from the HTML and not the CSS? Don’t love it. In my simple example, I use Pug to create a loop to do it.

#grid
  - const items = 10;
  - for (let i = 0; i < items; i++)
    div(style=`view-transition-name: item-${i};`)

That Pug code turns into:

<div id="grid">
  <div style="view-transition-name: item-0;"></div>
  <div style="view-transition-name: item-1;"></div>
  <div style="view-transition-name: item-2;"></div>
  <div style="view-transition-name: item-3;"></div>
  <div style="view-transition-name: item-4;"></div>
  <div style="view-transition-name: item-5;"></div>
  <div style="view-transition-name: item-6;"></div>
  <div style="view-transition-name: item-7;"></div>
  <div style="view-transition-name: item-8;"></div>
  <div style="view-transition-name: item-9;"></div>
</div>

Jen Simmons made the point about how odd this is.

This is being improved, I hear. The CSSWG has resolved to

Add three keywords, one for ID attribute, one for element identity, and one that does fallback between the two.

Which sounds likely we’ll be able to do something like:

#grid {
  > div {
    view-transition-name: auto; 
  }
}

This makes me think that it could break in cross-document view transitions, but… I don’t think it actually will if you use the id attribute on elements and the view-transition-name ends up being based on that. Should be sweet.

Customizing the Animation

We’ve got another issue here. It wasn’t just a Pug loop need to pull of the view transition staggering, it’s a Sass loop as well. That’s because in order to control the animation (applying an animation-delay which will achieve the staggering), we need to give a pseudo class selector the view-transition-name, which are all unique. So…

::view-transition-group(item-0) {
  animation-delay: 0s;
}
::view-transition-group(item-1) {
  animation-delay: 0.01s;
}
::view-transition-group(item-0) {
  animation-delay: 0.02s;
}
/* etc. */

That’s just as cumbersome as the HTML part, except maybe even more-so, as it’s less and less common we even have a CSS processor like Sass to help. If we do, we can do it like this:

@for $i from 0 through 9 {
  ::view-transition-group(item-#{$i}) {
    animation-delay: $i * 0.01s;
  }
}

Making Our Own Sibling Indexes with Custom Properties

How much do we need to delay each animation in order to stagger it? Well it should be a different timing, probably increasing slightly for each element.

1st element = 0s delay
2nd element = 0.01s delay
3rd element - 0.02s delay
etc

How do we know which element is the 1st, 2nd, 3rd, etc? Well we could use :nth-child(1), :nth-child(2) etc, but that saves us nothing. We still have super repetitive CSS that all but requires a CSS processor to manage.

Since we’re already applying unique view-transition-names at the HTML level, we could apply the element’s “index” at that level too, like:

#grid
  - const items = 10;
  - for (let i = 0; i < items; i++)
    div(style=`view-transition-name: item-${i}; --sibling-index: ${i};`) #{icons[i]}

Which gets us that index as a custom property:

<div id="grid">
  <div style="view-transition-name: item-0; --sibling-index: 0;"> </div>
  <div style="view-transition-name: item-1; --sibling-index: 1;"> </div>
  <div style="view-transition-name: item-2; --sibling-index: 2;"> </div>
  <div style="view-transition-name: item-3; --sibling-index: 3;"> </div>
  <div style="view-transition-name: item-4; --sibling-index: 4;"> </div>
  <div style="view-transition-name: item-5; --sibling-index: 5;"> </div>
  <div style="view-transition-name: item-6; --sibling-index: 6;"> </div>
  <div style="view-transition-name: item-7; --sibling-index: 7;"> </div>
  <div style="view-transition-name: item-8; --sibling-index: 8;"> </div>
  <div style="view-transition-name: item-9; --sibling-index: 9;"> </div>
</div>

… but does that actually help us?

Not really?

It seems like we should be able to use that value rather than the CSS processor value, like…

@for $i from 0 through 9 {
  ::view-transition-group(item-#{$i}) {
    animation-delay: calc(var(--sibling-index) * 0.01s);
  }
}

But there are two problems with this:

  1. We need the Sass loop anyway for the view transition names
  2. It doesn’t work

Lolz. There is something about the CSS custom property that doesn’t get applied do the ::view-transition-group like you would expect it to. Or at least *I* would expect it to. 🤷

Enter view-transition-class

There is a way to target and control the CSS animation of a selected bunch of elements at once, without having to apply a ::view-transition-group to individual elements. That’s like this:

#grid {
  > div {
    view-transition-class: item;
  }
}

Notice that’s class not name in the property name. Now we can use that to select all the elements rather than using a loop.

/* Matches a single element with `view-transition-name: item-5` */
::view-transition-group(item-5) {
  animation-delay: 0.05s;
}

/* Matches all elements with `view-transition-class: item` */
::view-transition-group(*.item) {
  animation-delay: 0.05s;
}

That *. syntax is what makes it use the class instead of the name. That’s how I understand it at least!

So with this, we’re getting closer to having staggering working without needing a CSS processor:

::view-transition-group(*.item) {
  animation-delay: calc(var(--sibling-index) * 0.01s);
}

Except: that doesn’t work. It doesn’t work because --sibling-index doesn’t seem available to the pseudo class selector we’re using there. I have no idea if that is a bug or not, but it feels like it is to me.

Real Sibling Index in CSS

We’re kinda “faking” sibling index with custom properties here, but we wouldn’t have to do that forever. The CSSWG has resolved:

sibling-count() and sibling-index() to css-values-5 ED

I’m told Chrome is going to throw engineering at it in Q4 2024, so we should see an implementation soon.

So then mayyyyybe we’d see this working:

::view-transition-group(*.item) {
  animation-delay: calc(sibling-index() * 0.01s);
}

Now that’s enabling view transitions staggering beautifully easily, so I’m going to cross my fingers there.

Random Stagger

And speaking of newfangled CSS, random() should be coming to native CSS at some point somewhat soon as well as I belive that’s been given the thumbs up. So rather than perfectly even staggering, we could do like…

::view-transition-group(*.item) {
  animation-delay: calc(random() * 0.01s);
}

Faking that with Sass if fun!

Sibling Count is Useful Too

Sometimes you need to know how many items there are also, so you can control timing and delays such that, for example, the last animation can end when the first one starts again. Here’s an example from Stephen Shaw with fakes values as Custom Properties showing how that would be used.

One line above could be written removing the need for custom properties:

/* before */
animation-delay: calc(2s * (var(--sibling-index) / var(--sibling-count)));

/* after */
animation-delay: calc(2s * (sibling-index() / sibling-count()));

Overflow is a small bummer

I just noticed while working on this particular demo that during a view transition, the elements that are animating are moved to something like a “top layer” in the document, meaning they do not respect the overflow of parent elements and whatnot. See example:

Don’t love that, but I’m sure there are huge tradeoffs that I’m just not aware of. I’ve been told this is actually a desirable trait of view transitions 🤷.

p.s. DevTools Can Inspect This Stuff

In Chrome-based browsers, open the Animations tab and slow down the animations way down.

The mid-animation, you can use that Pause icon to literally stop them. It’s just easier to see everything when it’s stopped. Then you’ll see a :view-transition element at the top of the DOM and you can drill into it an inspect what’s going on.

]]>
https://frontendmasters.com/blog/view-transitions-staggering/feed/ 3 4232