diff --git a/css-view-transitions-2/element-scoped-view-transitions.md b/css-view-transitions-2/element-scoped-view-transitions.md new file mode 100644 index 000000000000..af4e3cd530e8 --- /dev/null +++ b/css-view-transitions-2/element-scoped-view-transitions.md @@ -0,0 +1,417 @@ +# Element-Scoped View Transitions + +Element-Scoped View Transitions is an extension to the +[View Transition API][VT-api] to help developers perform transitions within the +scope of a DOM subtree. + +The new API looks like this: + +```js +element.startViewTransition(() => { + // Update the DOM somehow. +}); +``` + +This performs a same-document view transition similar to +[`document.startViewTransition()`][document-SVT], except that we are now calling +`startViewTransition()` on an arbitrary HTML element instead of the document. + +That element becomes the "scope" for the transition, which +means that it will host the [`::view-transition`][v-t-pseudo] pseudo-element +tree, and act as a container for the transition animations. + +## Motivation + +Element-Scoped View Transitions delivers four benefits to the developer that were not achievable before: + +* _Concurrent transitions:_ Two or more elements can run view transitions at the same + time without being aware of each other. For example, different component libraries + may each want to use view transitions and remain composable with each other. + +* _Transitions affected by ancestor properties:_ View transitions can render + inside a container that applies a clip, transform, or animation to it. For + example, a view transition may run inside content while that content is + scrolling. + +* _Smooth rendering outside the transition scope:_ View transitions have to [pause + rendering][render-suppression] while the DOM callback is running, but now we can + pause rendering in only part of the page. + +* _Transitions respect z-index:_ Non-transitioning content outside the scope + can now paint on top of the transitioning content. This is + useful for overlays such as menus and notification bars, which previously + could not stack in front of the pseudo-element tree. + +## Current status + +Element-Scoped View Transitions has been proposed to the CSS Working Group +([#9890](https://github.com/w3c/csswg-drafts/issues/9890)) as a change to the +[CSS View Transitions Module Level 2][css-view-transitions-2] specification, +and [passed review](https://github.com/w3ctag/design-reviews/issues/1188) +by the W3C Technical Architecture Group (TAG). + +Chrome 147 has +[shipped](https://groups.google.com/a/chromium.org/g/blink-dev/c/n1-oZUKaXHY/m/LqwtfSBWBQAJ) +Element-Scoped View Transitions. +See also [Chrome Platform Status](https://chromestatus.com/feature/5109852273377280) +and the [implementation tracking bug](https://crbug.com/394052227). +Older Chrome versions require the `--enable-features=ScopedViewTransitions` command-line flag. + +Here is a [**DEMO**](https://output.jsbin.com/runezug/quiet) of Element-Scoped View Transitions, +showing concurrent transitions, transitioning inside a scroller, nested scoped transitions, +and transitioning behind a higher z-index overlay. + +[VT-api]: https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API +[document-SVT]: https://developer.mozilla.org/en-US/docs/Web/API/Document/startViewTransition +[v-t-pseudo]: https://developer.mozilla.org/en-US/docs/Web/CSS/::view-transition +[css-view-transitions-2]: https://drafts.csswg.org/css-view-transitions-2/ +[render-suppression]: https://drafts.csswg.org/css-view-transitions/#document-rendering-suppression-for-view-transitions + +## How to use + +You can play with Element-Scoped View Transitions in Google Chrome today. + +* Use Chrome 147 or newer. + +* In your HTML, declare a scope element with one or more participants like this: + +```html + +
+
Hello
+
+``` + +* In your Javascript, call `startViewTransition` on the scope. Pass a callback that + modifies the participants. + +```html + +``` + +## Feedback wanted + +We're interested in feedback from the web developer community about +the shape of the Element-Scoped View Transitions API, and +use cases where the feature works well or didn't work as expected. + +You can share your feedback by commenting on +[CSS WG issue #9890](https://github.com/w3c/csswg-drafts/issues/9890). + +## Design + +### Pseudo-element tree + +The pseudo-element tree for a scoped view transition looks similar to the +[pseudo-element tree for a document view transition](https://drafts.csswg.org/css-view-transitions-1/#view-transition-pseudos), +except that it is associated with the scope instead of the +`` element. + +The example above produces the following DOM subtree during the transition: + +``` +div#scope +└─ ::view-transition + ├─ ::view-transition-group(root) + │ └─ ::view-transition-image-pair(root) + │ ├─ ::view-transition-old(root) + │ └─ ::view-transition-new(root) + └─ ::view-transition-group(greeting) + └─ ::view-transition-image-pair(greeting) + ├─ ::view-transition-old(greeting) + └─ ::view-transition-new(greeting) +``` + +The `::view-transition` pseudo-element is laid out as a +`position: absolute; inset: 0` child of the scope. However, see +[Self-participating scopes](#Self-participating-scopes) and +[Scroller scopes](#Scroller-scopes) below for some special aspects of the relationship +between the scope and its pseudo tree. + +### Algorithm + +The steps for an element-scoped view transition are based on the +[steps for a document view transition](https://drafts.csswg.org/css-view-transitions-1/#lifecycle) +with appropriate modifications. At a high level: + +1. Create the [`ViewTransition`](https://drafts.csswg.org/css-view-transitions-1/#viewtransition) object. + +2. At the next rendering opportunity, capture the painted output of each tagged + element in the scope's DOM subtree, and create the pseudo-element tree + with `::view-transition-old` pseudo-elements. A tagged element's geometry + information is computed relative to the scope. + +3. Invoke the callback passed to `startViewTransition`. + +4. Create the `::view-transition-new` pseudo-elements and set up the default + animations. + +5. Run the animations. + +6. Clean up by destroying the pseudo-element tree. + +Between steps 2 and 4, we [pause the rendering](#Pause-rendering) of the +scope's subtree, so that any DOM updates inside that subtree +that occur during the callback are not presented to the user prematurely. + +### Constraints + +* The scope must be a block container with `contain: layout`. This ensures that + it generates a [stacking context][stacking-contexts] so that its painted + output can be captured as an atomic unit. (`display: inline-block` is allowed.) + +> If the scope does not have `contain: layout`, it acquires the behavior of +> `contain: layout` while the transition is running. But it's recommended for +> the developer to set `contain: layout` explicitly, since toggling it can reflow +> the surrounding content. + +* There cannot be more than one active transition with the same scope. If a + transition is started on an element that is already running a transition, the + pre-existing transition is skipped. + +* A tagged element cannot participate (by generating a `::view-transition-group`) + in more than one active transition at the same time. If you try to start a + transition which would trigger this situation, it is skipped. (See + [#12323](https://github.com/w3c/csswg-drafts/issues/12323) for discussion of + which transition to skip.) + +Within these constraints it is possible for two view transitions to run +concurrently on different scopes, even if one is a descendant of the other. +This is important for independent web components to be composable. + +[stacking-contexts]: https://developer.mozilla.org/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context + +### Tag containment + +Because element-scoped view transitions are intended to enable composition (nesting of +unrelated components that both use transitions), developers need a way to avoid +tag collisions when choosing their `view-transition-name` values. + +A new style value, `view-transition-scope: all`, serves this purpose. + +> `view-transition-scope: all` was spelled `view-transition-scope: auto` before +> Chrome 147.0.7717.0 and `contain: view-transition` before Chrome 146.0.7652.0. +> See [CSS WG issue #13123](https://github.com/w3c/csswg-drafts/issues/13123). + +A element-scoped view transition looks for tagged participants, starting with the scope +itself. If this tag search encounters a descendant with `view-transition-scope: all`, +it ignores that element and everything inside it, on the assumption that those tags +belong to a different scope. + +> If the scope does not have `view-transition-scope: all`, it acquires the behavior of +> `view-transition-scope: all` while the transition is running. But it's recommended +> for the developer to set `view-transition-scope: all` explicitly, as this will +> guarantee that there is never a participant collision (see [constraints](#Constraints)). + +### Pause rendering + +The developer can asynchronously mutate the DOM during the `startViewTransition` +callback (which may return a Promise). To avoid presenting intermediate states +to the user, we must pause the rendering of the DOM being transitioned. + +Document view transitions pause the rendering of the entire document while the +callback is running, but Element-Scoped View Transitions will only pause the rendering +of the DOM subtree rooted at the scope. + +When the callback is finished and the transition animations are running, the +rendering is no longer paused, but each tagged element participating in the +transition has its rendering hoisted into the corresponding +`::view-transition-new` pseudo-element. (This is the same for element-scoped and +document-scoped view transitions.) + +### Transition root + +Now that view transitions are element-scoped, we want to make it easy for the developer +to determine which scope a `ViewTransition` object is associated with. +So we're adding a `transitionRoot` property: + +```js +interface ViewTransition { + ... + readonly attribute Element transitionRoot; + ... +}; +``` + +Example usage: + +```js +function processAnimations(transition) { + let anims = transition.transitionRoot.getAnimations() + ... +} +... +let transition = el.startViewTransition(); +transition.ready.then(() => processAnimations(transition)); +``` + +See [CSSWG resolution for the transitionRoot property](https://github.com/w3c/csswg-drafts/issues/9908#issuecomment-2165621635). + +### Self-participating scopes + +By default, the scope is a participant in its own transition ("self-participating") +as if it had `view-transition-name: root`. The developer can opt out of this behavior by +setting `view-transition-name: none` on the scope explicitly. + +> The default `view-transition-name: root` style on the scope is inside a special +> dynamic user-agent stylesheet that is only visible to the transition and will not +> be reflected in [getComputedStyle()](https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle). + +#### Interactivity + +The opt-out from self-participation can be combined with +`::view-transition { pointer-events: none }` to preserve +interactivity and hover effects for non-transitioning elements +within the scope. + +See [Keeping the page interactive while a View Transition is running](https://css-tricks.com/keeping-the-page-interactive-while-a-view-transition-is-running/). + +#### Transitionable scope properties + +If the scope is self-participating, all parts of its rendering can be transitioned, +including borders and box decorations (outline, box-shadow). This implies that the +transition animation can overflow the box bounds of the `::view-transition` pseudo-element. + +Effects such as CSS `opacity` on the scope can be transitioned as well — in other words, +they apply "on the inside" of the scope's capture, not "on the outside" of the +scope's transition pseudo-tree. + +> Unlike effects, changes to the CSS `transform` or the layout offset of the scope +> cannot be transitioned, as they directly affect the placement of the transition +> pseudo-tree itself. + +#### Ancestor transition participation + +A scope cannot directly participate in an ancestor transition, because we treat it +as `view-transition-scope: all` (see [Tag containment](#Tag-containment)). + +However, a +scope and its transition can render inside a container that is participating in an +ancestor transition. This is illustrated in the [demo](https://output.jsbin.com/runezug/quiet) +(enable and play the "transitioning ancestor"). + +### Scroller scopes + +A scope may also be a scroller — that is, it may have `overflow: auto` or `overflow: scroll`. + +Note that because scopes can be [self-participating](#Self-participating-scopes), the transition +pseudo-tree is not moved by the scope's scroll offset. It is also not clipped to the scope's +client area, which can lead to participants appearing to "pop out" of the scroller. + +If you are **opting out** of [self-participation](#Self-participating-scopes), your scope +probably should not be a scroller. Wrap your scope in a containing `
` that is a scroller +if you want the transition to run inside the scrolling contents. + +#### Automatic nesting + +If you have a **self-participating scroller scope**, we use +[Nested View Transition Groups](https://github.com/WICG/view-transitions/blob/main/nested-explainer.md) and set `::view-transition-group-children(root) { overflow: clip }` to ensure that non-root +participants are clipped to the scope's client area. + +See [#13420](https://github.com/w3c/csswg-drafts/issues/13420) for more information about this +behavior. + +#### Scrollbar padding + +If you have an auto-nested self-participating scroller scope as described above, +you may observe that the `::view-transition-group-children` incorrectly overlaps the scrollbars. +This is a known limitation of nested groups ([crbug.com/475236700](https://crbug.com/475236700)). + +You can work around this by setting `overflow-clip-margin: content-box` and +applying padding to the `::view-transition-group-children` corresponding to the +space occupied by the scrollbars ([demo](https://output.jsbin.com/xiruqev/quiet)). +(You might need to use Javascript to detect the existence and thickness of the scrollbar.) + +We are [considering](https://github.com/w3c/csswg-drafts/issues/13407) ways to +use `scrollbar-gutter` to incorporate this logic into the user-agent style sheet. + +## Alternatives Considered + +Here are things we could have done instead of element-scoped view transitions, and things +we could have done differently within element-scoped view transitions. + +### Nothing (status quo) + +We could stick with document-level view transitions and the limitations described +in the [Motivation](#Motivation) section. + +Developers have expressed that those limitations cause real problems. For example, +a content area may be the logical subject of a view transition, but other page elements +like tooltips and menus need to appear on top of the content area in the `z-index` order. +Developers end up adding `view-transition-name` to those overlaid elements, even though +they are not transitioning, just to keep them on top, which becomes a game of "whack-a-mole". + +The global nature of document-level view transitions is also incongruous with the web's +core values of composition and modularity. CSS and DOM in general are designed to facilitate +granular application of rendering features, and the "isolation" of features to subtrees +(see e.g. [`contain`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/contain)) +is important for performance and heterogeneous components. + +### Other scoping mechanisms + +View transitions could have been "scoped" to something other than an element. + +A limited form of scoping was already possible by running a view transition in an `