Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Tue, 22 Oct 2024 16:42:54 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.3 225069128 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
Single-Directionally Allowed Overflow https://frontendmasters.com/blog/single-directionally-allowed-overflow/ https://frontendmasters.com/blog/single-directionally-allowed-overflow/#comments Wed, 10 Jul 2024 22:09:38 +0000 https://frontendmasters.com/blog/?p=2985 There is this annoying thing in CSS where it feels like you should be able to hide overflow in one direction and allow it in another, since they can be separate properties:

.nav-bar {
  overflow-x: hidden;
  overflow-y: visible;
}

But you’ll be disappointed.

I really wish I could explain to you why you can’t do that, but I don’t know the historical CSS discussions that got us here. People clearly want to do it sometimes. I know, I’ve read tons of threads about it. The most common use case is something like an app sidebar along the left of a layout which can scroll vertically, but allows for menus that can extend out of it to the right. Another use case is a header bar on top of a site that hides the horizontal overflow but allows for vertical (sometimes a nice protection to avoid awkward “page zoom outs” with content that accidentally overflows).

Good news: you can do it with the clip value

This does work:

.nav-bar {
  overflow-x: clip;
  overflow-y: visible;
}

Here’s the proof:

The Caveats

Support is mostly fine, unless you worry about Safari 15.

Also, there is a difference between the values hidden and clip.

The hidden value does visually hide the overflow and will not add any visible scrollbars to the element. But! The hidden value does still technically allow you to scroll that element. You can force it sometimes with a mouse by highlighting text and dragging the direction of where the overflow is. And you can do it with JavaScript. The clip value does not allow you to do this. The content that is “clipped” away is truly inaccessible (visually).


Kilian Valkhof had a nice article about all this a while back, and also shows off the related overflow-clip-margin which is a nice bonus feature to clipping.

]]>
https://frontendmasters.com/blog/single-directionally-allowed-overflow/feed/ 2 2985