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:
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-name
s.
This brings me to my first point. Generating unique view-transition-name
s 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-name
s 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:
- We need the Sass loop anyway for the view transition names
- 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. 🤷
view-transition-class
Enter 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()
andsibling-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.
Hey Chris,
Here’s some answers to the questions you have:
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 transitionh1#big-title
on page A to page B because it will get aview-transition-name
ofbig-title
.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_e2czg0You 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.The new layered capturing mode will solve that. See https://codepen.io/bramus/pen/GRVRjYE for a demo (try it in Chrome Canary)
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).
Thanks!
If that’s just how
view-transition-name: auto
worked I wouldn’t be mad about it 🙂Published the same day, Roman Komarov with a bunch of stuff on sibling-*() stuff:
https://kizu.dev/tree-counting-and-random/