[css-view-transitions-2] Ignore offscreen elements from participating in transitions
Currently if an element has a non-none computed value for view-transition-name, it participates in the transition irrespective of whether it is in the visible viewport. This means the element will be rendered, which has significant computational and memory overhead, even if it is never seen by the user. If the developer wants to avoid this overhead, they have to keep track of the visibility of each element and only add view-transition-name to the onscreen ones.
The proposal to make this case easier is as follows:
- Add a new CSS property view-transition-offscreen which customizes the behavior for named elements based on their position in the snapshot viewport.
- The property has 2 values: auto and absent. auto indicates that the UA should render the element irrespective of its viewport position (as-if its onscreen). Would be nice to allow flexibility to the UA to optimize out such elements in case the transition is on memory constrained devices. That's why "should" instead of "must".
- absent indicates that if the element's ink overflow rectangle does not intersect the snapshot viewport, the element does not participate in the transition (as-if its view-transition-name's computed value was none).
- The computation for the element's viewport position for the decision above can be done here for old elements. And here for new elements. The subtlety is that for new elements we do it before resolving the ready promise so script can observe the final pseudo DOM structure.
See prior discussion on this here.
There are memory and computational benefits to this feature, but I originally proposed this as more of a visual / developer experience feature.
If two pages have a common heading, you may want that to be static in a transition, so you give it a page-transition-name.
However, if one of the pages is scrolled 8000px, the transition between the two will be bad, as the header will fly in from 8000px away.
With this feature, developers will be able to create a special incoming/outgoing animation for the header in this case (using the :only-child selector).
- The property has 2 values: auto and absent. auto indicates that the UA should render the element irrespective of its viewport position (as-if its onscreen). Would be nice to allow flexibility to the UA to optimize out such elements in case the transition is on memory constrained devices. That's why "should" instead of "must".
Since it significantly changes the animation, I'm not sure we can, or should, do this automatically. Since we won't be doing it in the first release, and it will only happen on constrained devices, I think it'll lead to things appearing broken when this 'auto' behaviour kicks in. I'd rather say that transitions can be skipped if the device is constrained, since that's a more reliable fallback that developers will already be catering for.
That said, I support the default value being auto, since elements are sometimes ignored due to content-visibility https://github.com/w3c/csswg-drafts/issues/7874
It feels like this is one of these options that you should give to startViewTransition to optimize things because you know that you've marked way too much with the view-transition-name. Although I do see the appeal of the new css property since you can use that in MPA cases too, to me it doesn't really feel like the right abstraction
I like it as a CSS property since the instruction can be per transition item. Eg, if you're reordering a list, you still want things to transition from outside the viewport. Whereas you might not want that behaviour for a header. Both might happen in the same transition.
A third value which would be useful for a case like https://deploy-preview-32--infrequently.netlify.app/, the page has a massive element which will be captured in entirety while the animation doesn't need that.
Syntax ideas, view-transition-offscreen: clip allows the UA to snapshot an intersection of the element's ink overflow rectangle with the visible viewport or snapshot root. Potential add-on is view-transition-offscreen: clip inset(10px), second argument allows specifying an explicit skirt by which the snapshot should be expanded in either direction.
Isn't the clipping just supposed to happen automatically? https://drafts.csswg.org/css-view-transitions-1/#compute-the-interest-rectangle-algorithm
Isn't the clipping just supposed to happen automatically? https://drafts.csswg.org/css-view-transitions-1/#compute-the-interest-rectangle-algorithm
That automatic clipping is very conservative, only if we must because of constraints like max texture size. Technically an implementation doesn't need to be constrained by it (you could create a tiled image), but the option gives UA flexibility.
This property would be an explicit hint from the developer that only the onscreen content (or a skirt around it) of this DOM element will be animated during the transition. The UA can use this knowledge to aggressively optimize for memory by painting and snapshotting a subset of the DOM element.
On a hackathon I coached at this was a dealbreaker for some, as they saw performance get tanked on non-highend devices. They were animating 1 element out of a list of 50, of which only 7 of them where visible in the viewport. Having a way to easily exclude these offscreen elements would surely be beneficial here.
Are they happy with some items being :only-child, and animating as such, even though they existed in both states.
I wonder if we need some other feature in that case, where the group still animates from old to new, but there's only a new in the pair, or the group is dropped if the final position of the group is still out of view.
I wonder if we need some other feature in that case, where the group still animates from old to new, but there's only a new in the pair, or the group is dropped if the final position of the group is still out of view.
@jakearchibald we have a couple of other issues with the characteristics you mentioned:
- https://github.com/w3c/csswg-drafts/issues/9354 does the "group is dropped if the final position of the group is still out of view". This is helpful for cases where you want an element to either morph (if its onscreen both sides) or undo capturing it on the old side.
- https://github.com/w3c/csswg-drafts/issues/9406 does the "the group still animates from old to new, but there's only a new in the pair". The browser can optimize quite a lot with this. We are forced to capture images for offscreen elements in the old DOM elements (because they will be destroyed) but there's no such requirement for the new DOM. If the pseudo displaying that image is not onscreen, we don't need to render it.
We haven't dug into the exact API shape but given how related these 3 features are, I feel like we should tackle them together. Like a new CSS property to specify one of these modes?
I like the direction this conversation was going. The main thing that justifies a new attribute for this is the idea that this could be a UX choice rather than a mere optimization (e.g. preventing a header from jumping).
I think the semantics here should be similar to content-visibility and intersection observers, however because this is observable and not just an optimization, the definitions need to be exact and customizable via a margin (same as rootMargin in IntersectionObserver).
Another thing I think we should do is make this new attribute inherited, this way the author can decide that a container makes its entire set of descendants behave in a certain way (and this can be overridden further down the tree).
Perhaps this can be view-transition-visibility or view-transition-overflow with:
-
visible: current behavior -
auto: similar tocontent-visibility: auto. element acts as if it doesn't have aview-transition-nameif it's not intersecting with the viewport. -
hidden: element ignores itsview-transition-name. As this attribute is inherited, this can be used to hide an entire tree from the view transitioning capture algorithm -
clip: the element participates in the transition, but only contents of the element that intersect with the viewport are captured..
+1 to making the property inherited. I'm assuming the initial value will be visible.
For the auto/clip case, is there any use-case to let developers expand the snapshot viewport by some margin. So any elements/area within that is considered visible. This can also be a future extension with the current capability limited to clipping at snapshot viewport boundary.
A few additional comments:
- I wouldn't do anything regarding the ink overflow. It shouldn't be web observable... so I think the intersection should work according to the IntersectionObserver rules and disregard ink-overflow.
- Not sure if
clipshould be something a different attribute. When the whole content is clipped-out, do you want to treat it as an empty image or not have it at all? e.g. perhaps still show the ink-overflow? something aboutclipfeels a bit different as it clips the content rather than removes the whole capture.
I wouldn't do anything regarding the ink overflow. It shouldn't be web observable... so I think the intersection should work according to the IntersectionObserver rules and disregard ink-overflow.
Hmmm, I think there will be cases where disregarding the ink overflow would end up with glitchy behaviour. Like a widget whose shadow is in the viewport. Should we really ignore it? This has come up for IO in the past too: https://github.com/w3c/csswg-drafts/issues/8649. @szager-chromium on that.
I was curious how content-visibility handles this. Looks like it relies on overflow-clip-margin (which is developer provides) and uses that here. Even if the overflow clip edge is further than the actual ink or scroll overflow, we rely on the specified edge.
With VT we intentionally decided not to have paint containment, so that won't work for us.
Not sure if clip should be something a different attribute. When the whole content is clipped-out, do you want to treat it as an empty image or not have it at all?
Oh I just read what you said carefully and I tend to agree. We should have separate properties which decide whether the element participates in the transition. If it is participating, then a separate property has knobs for deciding how its captured. So omitting clip sounds good.
In that regard, maybe the use-case in https://github.com/w3c/csswg-drafts/issues/9354 should eventually be handled by view-transition-visibility.
We should have separate properties which decide whether the element participates in the transition. If it is participating, then a separate property has knobs for deciding how its captured. So omitting
clipsounds good.
Absolutely agree.
I think there will be cases where disregarding the ink overflow would end up with glitchy behaviour.
Also agree. I think ink-overflow intersection with the viewport counts as visible in terms of a view transition, else you risk 'seeing double'.
I was curious how content-visibility handles this.
Yeah, content-visibility uses paint containment for this reason, so that we don't need to render the subtree to figure out the extent of the overflow, all of the needed information is on the box itself (border box + overflow-clip-margin)
One of the open questions for this feature is defining when an element is considered visible. There's 2 aspects to it:
-
Intersection with the viewport (snapshot root from VT perspective). Ideally we'd use the element's ink overflow rect for it. IntersectionObserver already uses ink overflow rect to detect occlusion here. And #8649 is in progress to make the ink overflow bounds interoperable.
-
Occlusion by other elements on the page. I haven't seen a use-case where a named element was occluded so I don't know if we need to consider it. Occlusion calculations are also more expensive so better if we can avoid it.
I'm pretty certain we don't need to care about occlusion. Just viewport intersection.
One of the open questions for this feature is defining when an element is considered visible. There's 2 aspects to it:
Intersection with the viewport (snapshot root from VT perspective). Ideally we'd use the element's ink overflow rect for it. IntersectionObserver already uses ink overflow rect to detect occlusion here. And #8649 is in progress to make the ink overflow bounds interoperable.
Occlusion by other elements on the page. I haven't seen a use-case where a named element was occluded so I don't know if we need to consider it. Occlusion calculations are also more expensive so better if we can avoid it.
In either case I think we should be consistent with IntersectionObserver. For viewport intersection IntersectionObserver doesn't take the ink overflow into account, so this shouldn't either. If IO exposed ink overflow in some way, eg for occlusion, we could consider doing the same.
Note that with a big enough root margin, being accurate about the ink overflow becomes less important.
hmm, I think the developer intent of this feature trumps consistency with intersection observer here. We can avoid using the word 'intersection' if that's where the problem is.
I'll ask Shopify folks though.
hmm, I think the developer intent of this feature trumps consistency with intersection observer here.
How is the developer intent here different from the developer intent in IntersectionObserver?
I think consistency is important here, having several APIs that rely on viewport-intersection and work in a slightly different manner would create confusing UX bugs and confusion. If we want to expose ink-overflow, we should do that in IntersectionObserver as well somehow.
I think the underlying question here is how we would expect elements to behave that are right outside the edge of the viewport. If this was just about performance, we could make some educated guess and tweak it. But this about excluding elements that are outside the viewport from participating.
So let's say you have an element that has a top: 100vh or some such - deliberately right outside the viewport. Should a 1px blur make it suddenly participate in the transition? Feels to me that this should not be something that relies on implementation-specific things and the author should be able to curate this with properties rather than rely on this unspeced behavior.
How is the developer intent here different from the developer intent in
IntersectionObserver?
IntersectionObserver is more about reacting to an element when it's in the viewport, that's why it has options for being completely in the viewport, and everything in between. It was originally created with lazy-loading in mind.
With view transitions, we're in a very visual space. So "not there" becomes more of a visual question than a layout question. If we count "partially visible" as "not there", then you'll end up with transitions where the old thing and the new thing are both visible (to some degree) in the transition, but they won't transition as part of the same group, so the user will see double. I don't think that's a desirable outcome of this feature.
Also, parts of intersection observer do consider ink overflow.
Also, parts of intersection observer do consider ink overflow.
There's an open proposal about occlusion, is that what you mean? Otherwise can you be specific?
The occlusion stuff is shipped in Chrome, no?
With view transitions, we're in a very visual space. So "not there" becomes more of a visual question than a layout question. If we count "partially visible" as "not there", then you'll end up with transitions where the old thing and the new thing are both visible (to some degree) in the transition, but they won't transition as part of the same group, so the user will see double. I don't think that's a desirable outcome of this feature.
I can totally see this point. The other side of this is that due to the implementation-specific nature of ink overflow, the outcome of using it here would be that in some cases you'd have elements that are visually 100% outside the viewport (on the edge) participate in the transition even if you gave them view-transition-visibility: auto - let's say they have a few pixels of blur that are reserved but not actually rendered, and you'd have no recourse to fixing it (except for maybe using IntersectionObserver yourself). Having a property for this (some viewport margin or element clip margin is enough) gives this control to the developer rather than to implementation details that can change at any time.
An alternative would be to actually specify some sort of max ink-overflow for the different visual effects (though glyphs also have an ink overflow which makes this a bit icky).
One thing we can do rather than rely on ink overflow specifically but would have the same effect, is to say that elements outside the viewport have an implementation-defined margin around them as a buffer, which can be overridden with a CSS property. This way if the author wants a very specific control of how this works they can do that, and the default is known to be implementation-defined.
running into quite a few use cases where having the captured transition views limited to just the viewport would be a requirement. upvote from me 👍
This would be really useful. running into quite a few issues where this would drastically improve transitions. upvote from me 👍
Would this be more about a performance optimization for you, or for design (avoiding things flying in/out of the screen)?