View Transitions Staggering

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;">Code language: HTML, XML (xml)

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};`)Code language: plaintext (plaintext)

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>Code language: HTML, XML (xml)

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; 
  }
}Code language: CSS (css)

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. */Code language: CSS (css)

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;
  }
}Code language: SCSS (scss)

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]}Code language: plaintext (plaintext)

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>Code language: HTML, XML (xml)

… 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);
  }
}Code language: SCSS (scss)

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;
  }
}
Code language: CSS (css)

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;
}
Code language: CSS (css)

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);
}Code language: CSS (css)

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);
}Code language: CSS (css)

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);
}Code language: CSS (css)

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()));Code language: JavaScript (javascript)

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.

Wanna learn CSS from a course?

3 responses to “View Transitions Staggering”

  1. Bramus says:

    Hey Chris,

    Here’s some answers to the questions you have:

    • Unique View Transition Names vs MPA:

    Correct, auto-generated names won’t play nice with MPA indeed because you get different nodes. Therefore we are also looking into giving you a way to create a view-transition-name that uses the value from the HTML id attribute. That way you can transition h1#big-title on page A to page B because it will get a view-transition-name of big-title.

    • Access to Custom Properties in the VT pseudos:

    The snapshots are nothing but pixels on a screen. As a result you can’t do things like color: red on a vt-pseudo. Same goes for access to custom properties of the original element: in the vt-pseudo you get a pixel representation, not the actual element. I cover this in my View Transition talks. See this clip, for example: https://www.youtube.com/clip/UgkxAtyHsIZJsTW17ERXGTK21GuPt_e2czg0

    • Staggering with sibling-index():

    You will be able to use sibling-index() on the ::view-transition-group pseudos. Combined with the upcoming layered capturing mode, you’ll be able to easily stagger the animations using just that.

    • Overflow bummer:

    The new layered capturing mode will solve that. See https://codepen.io/bramus/pen/GRVRjYE for a demo (try it in Chrome Canary)

    • DevTools:

    You can also pause the animations before they start 😉

    Feel free to ping me on any occasion should you have questions about View Transitions (and other CSS things).

    • Chris Coyier says:

      Thanks!

      That way you can transition h1#big-title on page A to page B because it will get a view-transition-name of big-title.

      If that’s just how view-transition-name: auto worked I wouldn’t be mad about it 🙂

  2. Chris Coyier says:

    Published the same day, Roman Komarov with a bunch of stuff on sibling-*() stuff:

    https://kizu.dev/tree-counting-and-random/

Leave a Reply

Your email address will not be published. Required fields are marked *

Did you know?

Frontend Masters Donates to open source projects. $313,806 contributed to date.