diff --git a/explainer.md b/explainer.md index 91f41eb..c9f2672 100644 --- a/explainer.md +++ b/explainer.md @@ -1,619 +1,439 @@ # Introduction -When a user navigates on the web, they get to see the inner workings of web -experiences: flash of white followed by a piece-meal rendering phase. This -sequenced user experience results in a higher cognitive load because the user -has to connect the dots between where they were, and where they are. Not only -that, a smooth animation to transition between scenes also reduces the loading -latency perceived by the user even if the actual loading time is the same. For -these reasons, most platforms provide easy to use primitives to enable -developers to build seamless transitions: -[Android](https://developer.android.com/training/transitions/start-activity), -[iOS/Mac](https://developer.apple.com/documentation/uikit/uimodaltransitionstyle) -and -[Windows](https://docs.microsoft.com/en-us/windows/apps/design/motion/page-transitions). - -Shared Element Transitions provides developers with the same capability on the -web, irrespective of whether the transitions are cross-document or -intra-document (SPA). +https://user-images.githubusercontent.com/93594/140955654-fa944c4d-530e-4d3c-8286-50864d59bb0d.mp4 + +When a user navigates on the web from Page-A to Page-B, the viewport jumps and there is a flash of white as elements disappear only to reappear in the same place in some in-progress state. This sequenced, disconnected user experience is disorienting and results in a higher-cognitive load as the user is forced to piece together how they got to where they came from. Additionally, this jarring experience increases how much users perceive the page loading as they stare at the white limbo state. + +Smooth loading animations can lower the cognitive load by helping users [stay in context](https://www.smashingmagazine.com/2013/10/smart-transitions-in-user-experience-design/) as they navigate from Page-A to Page-B, and [reduce the perceived latency](https://wp-rocket.me/blog/perceived-performance-need-optimize/#:~:text=1.%20Use%20activity%20and%20progress%20indicators) of loading by providing them with something engaging and delightful in the meantime. For these reasons, most platforms provide easy-to-use primitives that enable developers to build seamless transitions: [Android](https://developer.android.com/training/transitions/start-activity), [iOS/Mac](https://developer.apple.com/documentation/uikit/uimodaltransitionstyle) and [Windows](https://docs.microsoft.com/en-us/windows/apps/design/motion/page-transitions). + +https://user-images.githubusercontent.com/93594/141100217-ba1fa157-cd79-4a9d-b3b4-67484d3c7dbf.mp4 + +Shared Element Transitions provides developers with the same capability on the web, irrespective of whether the transitions are cross-document (MPA) or intra-document (SPA). # Use-Cases -A visual demo of some example transition patterns targeted by this feature are -[here](https://material.io/design/motion/the-motion-system.html#transition-patterns). -The following is a summary of the semantics of these transition patterns : - -* Root Transitions : The full page content animates between two web pages with - an optional static UI element on top. - [Shared axis](https://material.io/design/motion/the-motion-system.html#shared-axis) - shows an example. -* Shared Element Transitions : A persistent UI element morphs into another - (which could be a UI element on the next page or the whole page) changing - its shape and content. - [Container transform](https://material.io/design/motion/the-motion-system.html#container-transform) - shows an example. -* Entry/Exit Transitions : A UI element animates as it exits or enters the - screen. This - [issue](https://github.com/WICG/shared-element-transitions/issues/37) shows - an example. - -These transitions should be feasible in SPAs (Single Page Apps) and MPAs (Multi -Page Apps). +A visual demo of some example transition patterns targeted by this feature is [here](https://material.io/design/motion/the-motion-system.html#transition-patterns). The following is a summary of the semantics of these transition patterns: + +* Root Transitions: The full page content animates between two web pages with an optional static UI element on top. Examples 1 & 2 [here](https://material.io/design/motion/the-motion-system.html#shared-axis) are demonstrations of this. +* Shared Element to Root Transitions: A persistent UI element morphs into the full page content on the next web page. [Container transform](https://material.io/design/motion/the-motion-system.html#container-transform) shows an example. +* Shared Element Transitions: A persistent UI element morphs into another UI element on the next web page. The element's contents and shape can change during this transition. This [video](https://www.youtube.com/watch?v=SGnZN3NE0jA) shows an example. +* Entry/Exit Transitions: A UI element animates as it exits or enters the screen. This [issue](https://github.com/WICG/shared-element-transitions/issues/37) shows an example. # Design -Let's take the example below which shows how the API can be used by a developer -to animate the background and a shared element on a same origin navigation -(MPA). The SPA equivalent of this case is one where the old document is mutated -into the new document via DOM APIs. See [API Extensions](#api-extensions) for -more code examples. - -Note that the API shape below is tentative and used to explain the core feature -design. - -### Old Document - -```html - - - - - - - Click Me - - -``` +The goal is to provide a mechanism and API which will allow simple transitions like above to be specified in CSS, building on CSS animations, but also allow for more complex transitions to be performed via JavaScript, building on the Web Animations API. + +This section covers the concepts and mechanisms, while a later section looks at possible API shapes. + +Performing a transition from Page-A to Page-B requires parts of both to be on screen at the same time, potentially moving independently. This is currently impossible in a cross-document navigation, but it's still hard in an SPA (single page app) navigation. You need to make sure that the outgoing state persists along with the incoming state, that it can't receive additional interactions, and ensure the presence of both states doesn't create a confusing experience for those using accessibility technology. + +The aim of this design is to allow for representations of both Page-A and Page-B to exist at the same time, without the usability, accessibility, performance, security and memory concerns of having both complete DOM trees alive. + +Here's the example that will be used to explain the design: + +Page-A and Page-B + +The concepts and process described in this section apply for both MPA and SPA transition, however the API will differ in parts. + +Cross-origin transitions are something we want to tackle, but may have significant differences and restrictions for security reasons. Cross-origin transitions are not covered in this document. + +## Part 1: The offering + +Before Page-A goes away, it offers up elements to be used in the transition. Generally, this will mean an element that animates independently during the transition. For the example transition, the elements are: + +- The header +- The share button +- The rest (referred to as the page root) + +https://user-images.githubusercontent.com/93594/141104275-6d1fb67a-2f73-41e4-9cef-14676798223b.mp4 + +Aside from the root, an element offered for a transition has the following restrictions: + +- [`contain: paint`](https://developer.mozilla.org/en-US/docs/Web/CSS/contain) which ensures that the element is the containing block for all positioned descendants and generates a stacking context. This implies that the child content will be clipped to the context-box but it can be expanded using ['overflow-clip-margin'](https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-clip-margin). Being a stacking context and a containing block allows the element to be treated as a single unit, whereas paint containment simplifies implementation. +- [`break-inside: avoid`](https://developer.mozilla.org/en-US/docs/Web/CSS/break-inside) which disallows fragmentation ensuring the element content is a single rect, i.e., it doesn't break across lines or columns, again allowing the element to be treated as a single unit. + +These constraints are implicitly applied to the element's rendering by the UA. See [issue](https://github.com/WICG/shared-element-transitions/issues/71) for detailed discussion. + +When a developer offers elements for a transition, there are two modes they can choose from: + +### As a single image + +The entire painting of the element is captured, including things which appear outside of its bounding box such as shadows and blurs, as a single CSS image. + +https://user-images.githubusercontent.com/93594/141118353-d62d19a1-0964-4fa0-880f-bdde656ce899.mp4 + +The element is captured without the effects (such as opacity and filters) from parent elements. Effects on the element itself are baked into the image. However, the element is captured without transforms, as those transforms are reapplied later. The root is always captured as a single image, with the other transition elements removed (similar to how compositing works today). + +Capturing an element in this way isn't a new concept to the platform, as [`element()`]() in CSS performs a similar action. The differences are documented later. + +Capturing as a CSS image avoids the interactivity risks, complexities, and memory impact of fully preserving these parts of Page-A as live DOM. On the other hand, it means that the capture will be 'static'. If it includes things like gifs, video, or other animating content, they'll be frozen on the frame they were displaying when captured. + +#### Image Size +The size of the image cached for an element is equal to the element's [ink overflow rectangle](https://drafts.csswg.org/css-overflow-3/#ink-overflow-rectangle). This allows exposing parts of an element during the transition which may have been hidden earlier. The user-agent is allowed to clip the image to an implementation defined size (a common case would be the max texture size supported by the device). When caching a subset of the element due to this constraint, the area within the element cached by the user-agent is the area closest to the viewport. + +The size of the root image and the area captured follows a pattern similar to shared elements. However, since the root image is generated using the root stacking context it is likely to be clipped to an implementation defined size in most cases. + +An alternate choice was to clip the element to viewport bounds to limit memory use, particularly for the root element. This can be added as a perfomance hint from the developer in future iterations. See issues [72](https://github.com/WICG/shared-element-transitions/issues/72) and [73](https://github.com/WICG/shared-element-transitions/issues/73) for detailed discussion on this topic. + +The single image mode works great for the share button and the root, as their transitions can be represented by simple transforms. However, the header changes size without stretching its shadow, and the content of the header moves independently and doesn't stretch. There's another mode for that: + +### As the element's computed style + content image + +In this mode the computed styles of the element are copied over, so they can be re-rendered beyond just transforming an image. + +The children of the element (including pseudos and text nodes) are captured into a single CSS image that can be animated independently. + +This allows the developer to animate [animatable CSS properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties) on the container such as border, background-color, border-radius, opacity. These properties roughly map to the element's box decorations and visual effects. The developer isn't prevented from animating properties like `font-size` but, since the children are captured as an image, changing the `font-size` won't change the visual output. + +https://user-images.githubusercontent.com/93594/141118395-8d65da49-a5ab-41c6-8458-917e55d4b77b.mp4 + +A mode like this is unnecessary complexity for the share button in the example transition, but allows creating richer effects for the header transition. + +The second mode where styles are copied to a container element won't be part of 'v1' of this feature. + +### Nested transition elements -### New Document - -```html - - - - - - - - +In the example transition, the content of the header cross-fades from Page-A to Page-B. An even smoother transition could be achieved by also animating the site title and avatar 'chip' independently. To allow for this, an offered element can contain other offered elements. + +When an element is captured, its painting is 'lifted' from the parent offered element, which may be the root. This is similar to how browsers handle composited elements. + +### Retaining hierarchy during a transition + +By default, offered elements are captured in a flat hierarchy. As in all offered elements, including the root, will be laid out as siblings. The browser uses cached transforms to position each element so it overlaps exactly with its quad on the old page. + +The means that, during the transition, scaling one offered element won't impact the rendering of another offered element, even if it was a child of the other element in the old page. + +This mode allows elements to visually move between containers in viewport space, even if they were clipped to some parent in old page. + +Alternatively, the developer can make an offered element a 'transition container'. Offered elements will be nested within their closest transition container, and the cached transform will position the element within that container rather than the viewport. This is similar in spirit to how `position: relative` becomes the containing block for absolutely positioned elements. + +If 'transition containers' are used in combination with "element's computed style + content image", then effects on the parent such as `opacity`, `filter`, `mix-blend-mode` will also carry through to the children. Whereas in the flattened model, and "single image" model, effects on the parent no longer apply to the children. + +'Transition containers' are out of scope for 'v1' of the feature. See [issue](https://github.com/WICG/shared-element-transitions/issues/74) for detailed discussion. + +## Part 2: The preparation + +At this point the state has changed over to Page-B, and Page-A is gone aside from the elements it offered. In the MPA case, this happens when the navigation is complete. In the SPA case, this happens when the DOM is in the Page-B state and the developer signals that the change is complete (how to make that signal is discussed later in the API). + +### Setting the stage + +The captured elements are displayed using a tree of pseudo-elements. The root of this tree is a viewport-filling container with fixed position. The offered elements from Page-A are positioned absolutely at (0,0), nested according to their closest 'transition container' or the root if there's no parent container, and moved into their previous viewport-relative (or transition container relative) positions using the cached transform. Their content is painted on top of Page-B which ensures that the user continues to see Page-A's visuals as Page-B is loading. Note that this may not reproduce the exact rendering on Page-A. For example, the relative paint order of shared elements is preserved in this tree rendered on top of Page-B. But if a shared element was occluded by another element, the latter is painted into the root's image unless it is also offered as a shared element. + +Page-B is hidden from rendering until the transition is complete. + +### How are transition elements represented? + +The CSS images and computed properties/styles cached from Page-A are represented as elements with the following nesting: + +``` +transition root +├─ transition element +│ ├─ image wrapper +│ │ └─ image +│ └─ …child transition elements… +└─ …other transition elements… ``` -The steps taken by the browser during the transition are as follows. - -1. When the user presses "Click Me" and a navigation is initiated on the old - Document, create the following pseudo-elements in the top layer[^1]. Note - that a shared element must not be nested inside another shared element : - - a. A container pseudo-element and child replaced pseudo-element for each - element with the `sharedid` attribute. These are identified - via ::shared-container(shared_id) and ::shared-old(shared_id) respectively, - where shared_id is the value of the `sharedid` attribute. - - b. A replaced pseudo-element for the root/html element identified - via ::shared-old-root. - - c. The box hierarchy in the top layer stacking context is : - - ``` - ├───shared-old-root - ├───shared-container(header-id) - ├───shared-old(header-id) - ``` - -2. Apply the following UA stylesheet to the pseudo elements on the old page : - - ```css - ::shared-old-root, ::shared-container(header-id) { - position: fixed; - top: 0px; - left: 0px; - } - - ::shared-old-root { - width: 100vw; - height: 100vh; - // The output of element() function on the root element. - content: element(html); - } - - ::shared-container(header-id) { - /* This size is is chosen exactly according to the header-id element's - border box dimensions after layout. */ - width: 100px; - height: 100px; - - /* A transform positioning the element relative to the viewport so that it overlaps - exactly with its screen coordinates and orientation it had on the old page. */ - transform: translate(8px, 308px); - } - - ::shared-old(header-id) { - position: absolute; - width: 100%; - height: 100%; - content: element(header-id); - } - ``` - -3. Save the output of the element() function for each pseudo element referenced - above with the computed size and transform applied to container elements. - -4. Navigate to the new page leaving the last rendered pixels of the old page on - screen. - -5. When the new page loads, suppress rendering until resources required for - first render have been fetched. - -6. Once the page is ready for first render, create the following pseudo - elements in the top layer. The pseudo elements are kept in sync with the - corresponding shared elements in the DOM until the transition completes as - specified in step 8 : - - a. A container pseudo-element and child replaced pseudo-element for each - shared element on the old page using state saved in step 3. - - b. A container pseudo-element and child replaced pseudo-element for each - element with the `sharedid` attribute on the new page. The container is - reused if already present. - - c. A replaced element for the root/html element identified - via ::shared-new-root. - - d. The box hierarchy in the top layer stacking context is : - - ``` - ├───shared-new-root - ├───shared-old-root - ├───shared-container(header-id) - ├───shared-new(header-id) - ├───shared-old(header-id) - ``` - -7. Apply the following UA stylesheet to the pseudo elements on the new page. - - ```css - ::shared-old-root, ::shared-new-root, ::shared-container(header-id) { - position: fixed; - top: 0px; - left: 0px; - } - - ::shared-old-root, ::shared-new-root { - width: 100vw; - height: 100vh; - content: element(html); - } - - /* Update the container's size and transform to the shared element on the new page. */ - ::shared-container(header-id) { - width: 200px; - height: 200px; - transform: translate(8px, 108px); - - /* The blend mode referenced below is not currently exposed with mix-blend-mode. */ - isolation: isolate; - mix-blend-mode: [plus-lighter](https://drafts.fxtf.org/compositing/#porterduffcompositingoperators_plus_lighter); - } - - ::shared-old(header-id), ::shared-new(header-id) { - position: absolute; - width: 100%; - height: 100%; - } - - ::shared-old-root { - /* This is the saved output referenced in step 3. */ - content: cached-element(html); - } - ::shared-old(header-id) { - content: cached-element(header-id); - } - - ::shared-new-root { - content: element(html); - } - ::shared-new(header-id) { - content: element(header-id); - } - - /* Default animations added by the UA which can be overridden by the developer stylesheet/script. */ - @keyframes ::shared-new-fade-in { - 0% {opacity: 0;} - 100% {opacity: 1;} - } - ::shared-new-root, ::shared-new(header-id) { - animation: ::shared-new-fade-in 0.25s; - } - - @keyframes ::shared-old-fade-out { - 0% {opacity: 1;} - 100% {opacity: 0;} - } - ::shared-old-root, ::shared-old(header-id) { - animation: ::shared-old-fade-out 0.25s; - } - - /* Generated for each shared element with the syntax shared-container-sharedid. */ - @keyframes ::shared-container-header-id { - from { - width: 100px; - height: 100px; - transform: translate(8px, 308px); - } - } - ::shared-container(header-id) { - animation: ::shared-container-header-id 0.25s; - } - ``` - -8. When the transition finishes, remove all pseudo elements from the top layer. - The transition finishes when there is no active animation on any pseudo - element. See - [issue 64](https://github.com/WICG/shared-element-transitions/issues/64) for - discussion on this. - -An example simulating the steps above using the existing `element()` function -is [here](https://jsbin.com/bisoleziyi/edit?html,output) (open in Firefox). - -[![Video Link for Shared Element Transition](https://img.youtube.com/vi/QzGEBUW-3U8/0.jpg)](https://youtu.be/QzGEBUW-3U8) - -## Animating Box Decoration CSS Properties - -A common capability desirable during transitions is to interpolate styles like -border-radius that change the element's shape. The -[container transform](https://material.io/design/motion/the-motion-system.html#container-transform) -examples show a visual demo of that. Painting properties like the element's -border within its image when using the element() function makes this difficult. - -Consider the same example as above with the addition of box decorations to -the shared element. - -### Old Document - -```html - - - - - - - - +- **transition root**: The fixed position container which is the root of the pseudo element tree. This element has a width/height of the viewport. +- **transition element**: If the element is created as a "computed style + content image", this element will have a width and height of the content box of the original element, and have its computed styles reapplied. If the part is created as a "single image", this element will have a width and height of the border box of the original element. In either case, this element is absolutely positioned at 0, 0 and has a transform applied to position it in viewport space. +- **image wrapper**: This element is absolutely positioned with an `inset` of 0, and has [`isolation: isolate`](https://developer.mozilla.org/en-US/docs/Web/CSS/isolation). This wrapper is useful when cross-fading images (documented later). +- **image**: This contains the cached image, which may paint outside the parent elements. This would be a replaced element so CSS properties like `object-fit` will be supported. This element is absolutely positioned at 0, 0 and has a width and height of 100%, although the image may paint outside of its own bounds, similar to how a `box-shadow` is painted outside of an element's bounds. +- **child transition elements**: If this transition element is a 'transition container', child transition elements will be nested here. + +These elements will be accessible to the developer via pseudo-elements. The default animations specified by the user agent are set up using a dynamic user agent stylesheet. This allows developers to customize the transition by overriding the default styles with developer provided CSS. + +### Mixing in elements from Page-B and associating them with transition elements from Page-A + +At this stage, Page-B identifies elements on its own page to be involved in the transition. This happens in the same way as the offering phase with one difference: The images and styles from Page-B will be updated if the underlying page updates. This means things like animated gifs will play, rather than being frozen on whatever frame they were on when they were captured. + +The developer can associate particular elements from Page-A to elements from Page-B. This would usually be done if they're equivalent. In this case, the headers, share buttons, and roots are equivalent. When this happens, the image from the Page-B element is added to the same image wrapper: + +``` +transition element +├─ image wrapper +│ ├─ image (Page-A) +│ └─ image (Page-B) +└─ …child transition elements… ``` -### New Document - -```html - - - - - - - - +This allows for the container to be moved as one, while cross-fading the Page-A and Page-B content. The developer will also have access to the state of shared elements (from Page-A and Page-B) replicated on the container. This state depends on the capture mode (single image vs computed styles + content image). + +Transition elements don't need to be associated with another transition elements, which allows for transitions involving elements that are only in Page-A or only in Page-B. + +The root elements of each page are automatically associated. + +Note that the order in which the transition elements are painted can be configured by UA and/or developer stylesheets using z-index. + +- [Open question](https://github.com/WICG/shared-element-transitions/issues/23): How should the default UA animation order these elements? And also handle a change in associated elements between the 2 pages. + +### How are transition elements painted? + +During the transition a new stacking context (called uber-root) is created with the following hierarchy : + +``` +uber-root stacking context +├─ root stacking context +└─ transition stacking context ``` -The additional steps taken by the browser in the example above are : +This allows using the output of the root stacking context to provide a live root image for Page-B. The transition stacking context maps to the transition-root element. An alternate approach to this was to paint the transition-root in the [top layer](https://fullscreen.spec.whatwg.org/#top-layer). But that made it difficult to support transitions when there is other content in the top layer (fullscreen elements, dialog) and to ensure effects on the root element which are applied to the root stacking context (background-color, filter) are captured in the root image. See [issue](https://github.com/WICG/shared-element-transitions/issues/74) for detailed discussion. -i. When a pseudo element is created for shared elements in the old page in step -1, the pseudo-class "transition" is enabled for each shared element. +## Part 3: The transition -ii. In step 3 when saving state from the old page, the completed ComputedStyle -on the pseudo elements is saved in addition to the computed size and transform. +Everything is now in place to perform the transition. The developer can animate the transition elements created by the UA using the usual APIs, such as CSS and web animations. -iii. In step 7 when applying a UA stylesheet to pseudo elements on the new page, -the saved style is also applied to the old pseudo elements : +https://user-images.githubusercontent.com/93594/141100217-ba1fa157-cd79-4a9d-b3b4-67484d3c7dbf.mp4 -```css -::shared-container(header-id) { - /* This is the saved output referenced in step 3. Applied before updating to - values from the shared element in the new DOM. */ - width: 100px; - height: 100px; - transform: translate(8px, 308px); - border: 10px solid black; - border-radius: 10% 10%; - box-shadow: 0px 0px 10px; -} +Note that the browser defers displaying elements from Page-B and starting the animation until Page-B is ready for first render. This is currently driven by internal browser heuristics and is being standardized in the proposal [here](https://github.com/whatwg/html/issues/7131). + +## Part 4: The end + +When the transition is complete, the transition elements created by the UA are removed, revealing the real Page-B. The transition completes once no pseudo element has an active animation. + +# The MPA API + +There are a lot of open questions around the API, but this section will give a flavor of the direction. + +Little thought has been giving to naming in these APIs, which will be improved when there's stronger consensus around the concepts. + +## Creating a transition element -::shared-old(header-id) { - position: absolute; - width: 100%; - height: 100%; +### CSS properties - /* This is the saved output referenced in step 3. */ - content: cached-element(header-id); - top: -10px; - left: -10px; +```css +.header { + page-transition-tag: header; + page-transition-capture: container-and-child; } ``` -An example simulating the steps above using the existing `element()` function -is [here](https://jsbin.com/vesokanumu/edit?html,output) (open in Firefox). +Setting a `page-transition-tag` marks this element as a transition element, and the `page-transition-capture` property is used to indicate how it's captured (as a single image vs as a computed style + content image). + +Multiple elements could have the same `page-transition-tag` value, which is useful for aggregate content such as user comments. -[![Video Link for Shared Element Transition](https://img.youtube.com/vi/SGnZN3NE0jA/0.jpg)](https://youtu.be/SGnZN3NE0jA) +Elements on Page-A and Page-B which have the same `page-transition-tag` are automatically associated with each other. If multiple elements have the same value, they're associated in DOM order. -## Modifications to element() +Using CSS properties means values can change depending on viewport (`@media`) and browser support (`@supports`). -The following changes will be made to the element() spec as a part of this -proposal. The element captured by this function is the target element : +### JS API -* The target element must have paint containment (contain:paint) to ensure the - element is the containing block for all positioned descendents and generates - a stacking context. -* The target element must disallow fragmentation (similar to - `break-inside:avoid`). -* A new cached-element() function is introduced to refer to the saved output - of the element() function in step 3 of the design. (This part may not be - developer exposed though.) -* Nested shared elements are omitted from the output of element() function. -* Elements captured using element() and displayed via pseudo-elements during - the transition are not painted in the regular DOM - they behave as if they - have `content-visibility: hidden`, except that they don't have - `contain:size`. -* The special cases when running the `element()` function on the html element - are : - * The natural size for the generated image is the visual viewport bounds. - * When creating the image, the element is drawn on a canvas with the - background color of the document. +```js +// In Page-A +addEventListener('navigate', (event) => { + event.prepareSameOriginPageTransition((transition) => { + transition.offerElement('header', document.querySelector('.header'), { + capture: 'container-and-child', + }); + + transition.setData({…}); + }); +}); +``` -# API Extensions +Expanding on the capabilities of the CSS properties, a JS API allows the developer to change which parts are offered depending on the destination of the navigation, and the direction of navigation (back vs forward). This builds on top of the [app-history API](https://github.com/WICG/app-history). -## SPA +Also, data can be provided via `setData`. This can be anything structured-clonable, and will be made available to Page-B. -The SPA code requires the addition of script APIs which provide the equivalent -of "navigation" and "ready for first render" events referenced in step 1 and 4 -of the design. The rest of the code is identical between MPA and SPA. +- [Open question](https://github.com/WICG/shared-element-transitions/issues/78): Do we need `offerElement`? The same thing could be done by adding the CSS properties. ```js -function handleTransition() { - document.documentTransition.prepare(async () => { - await loadNextPage(); +// In Page-B +document.performTransition((transition) => { + console.log(transition.data); + const pageAHeader = transition.offeredElements.get('header'); + const pageBHeader = transition.createTransitionElement(document.querySelector('.header'), { + capture: 'container-and-child', }); + transition.matchElements(pageAHeader, pageBHeader); +}); +``` + +This sketch is particularly half-baked. A more concrete proposal will be possible when more of the concepts are decided. + +- [Open question](https://github.com/WICG/shared-element-transitions/issues/79): Do we need `createTransitionElement`? It could be done via adding CSS properties, but it might be clumsy if the developer is going to immediately remove the properties afterwards. +- [Open question](https://github.com/WICG/shared-element-transitions/issues/80): How does the outgoing page offer just the root for transition? +- [Open question](https://github.com/WICG/shared-element-transitions/issues/81): When is the `performTransition` callback called? + +## Defining the animation + +How will Page-B define the animation? + +### Default animation + +The potential default animations setup for the different cases are as follows. Note all of these can be overridden by the developer: + +- If an element exists in Page-A only (exit animation), a fade animation takes its container from opacity 1 to 0. +- If an element exists in Page-B only (entry animation), a fade animation takes its container from opacity 0 to 1. +- If an element exists in both Page-A and Page-B, and both are 'container and content', an animation takes the container from Page-A styles to Page-B styles (which will include the transform used for positioning), while cross-fading the two images. +- If an element exists in both Page-A and Page-B, and neither are 'container and child', a transform animation takes its container from Page-A size/transform to Page-B size/transform, while cross-fading the two images. +- If an element exists in both Page-A and Page-B and only one of them is 'container and content', both are forced to use 'single image' mode and use the default animation described above. See [issue](https://github.com/WICG/shared-element-transitions/issues/82) for detailed discussion. + +Because the images are sized to 100% of the container, the images will also change size throughout the transition. How these are scaled can be changed using regular CSS features like `object-fit`. + +In all cases, the duration and easing is some undecided default, that could even be platform independent. + +- [Open question](https://github.com/WICG/shared-element-transitions/issues/84): Default animations work well for things which are at least partially in-viewport in both Page-A and Page-B, but it gets tricky if you consider a non-sticky header that scrolled out of view by 1000s of pixels. +- [Open question](https://github.com/WICG/shared-element-transitions/issues/85): If the developer wants a default animation of the root only, how do they define that? + +### CSS animation + +CSS can be used to build on default animations, or override the default. + +```css +::page-transition-container(header) { + /* … */ } ``` -* The prepare API initiates step 1 to 3 to save the state of shared elements - in the current DOM. The API takes a callback once the save operation - finishes executing. -* The async callback initiates load of the next page and initiates step 4 - which suppresses rendering. -* When the callback returns, the new DOM is considered ready for first render. - This starts step 5 onwards to create new pseudo elements and start - animations. +Element selectors: + +- `::page-transition-container(name)` - Select the transition containers of a given `page-transition-tag`. +- `::page-transition-image-wrapper(page-transition-tag)` - Select the transition parts of a given `page-transition-tag`. +- `::page-transition-outgoing-image(page-transition-tag)` - Select the outgoing image of a given `page-transition-tag`. +- `::page-transition-incoming-image(page-transition-tag)` - Select the incoming image of a given `page-transition-tag`. +- `::page-transition-root-outgoing` - Select the outgoing root image. +- `::page-transition-root-incoming` - Select the incoming root image. +- `::page-transition-root-container` - Useful for cases where something needs to be rendered underneath the root images. + +These will be selecting UA generated pseudo-elements. + +```css +::page-transition-container(header) { + animation-delay: 300ms; +} +``` -## Additional Script APIs +CSS can be used to make changes to the default animation, or override `animation-name` to remove the default. -The following example shows how developers can configure the transition in -script for an MPA. +### JavaScript access to elements -### Old Document +So far the transition elements have been addressed by pseudo-element selectors, but JavaScript could be given access to the elements using the [CSSPseudoElement](https://drafts.csswg.org/css-pseudo-4/#CSSPseudoElement-interface) interface. ```js -addEventListener("navigate", (event) => { - // Add sharedid attribute to elements to offer for the transition - // based on current document state. - document.querySelector(".header").sharedid="header-id"; - - // setData can be used to pass opaque contextual information to the - // new page. The argument type is |any|. - document.documentTransition.setData({ version: 123 }); +// In Page-B +document.performTransition((transition) => { + // Build up transition parts, then… + document.documentElement.pseudo("page-transition-container(name)").animate(...); }); ``` -### New Document +- [Open question](https://github.com/WICG/shared-element-transitions/issues/86) What's the deadline for calling `performTransition`? + +Note that using pseudo-elements here implies that the developer can not change the DOM structure of transition elements. An alternate approach considered to provide this extensibility uses shadow DOM. The pseudo-element option was preferred to make it easier to build on concepts which already exist in the platform and ensure ease of implementation. See [issue](https://github.com/WICG/shared-element-transitions/issues/93) for detailed discussion. + +- [Open question](https://github.com/WICG/shared-element-transitions/issues/87): Is the freedom above a feature or a bug? + +## Signaling the end of a page transition + +At the start of the transition, the browser could gather all the `Animation`s active on the stage, and assume the animation is complete once all animations finish. + +In addition, the JS API could include a way to override this by letting the developer provide a promise which keeps the transition active, allowing for animations driven some other way, such as `requestAnimationFrame`. + +This is discussed in [#64](https://github.com/WICG/shared-element-transitions/issues/64). + +## Example + +Using the sketches above, here's how the example Page-A to Page-B transition could be done: + +https://user-images.githubusercontent.com/93594/141100217-ba1fa157-cd79-4a9d-b3b4-67484d3c7dbf.mp4 + +In Page-A: + +```css +.header { + page-transition-tag: header; + page-transition-capture: container-and-child; +} + +.share-button { + page-transition-tag: share-button; +} +``` + +In Page-B: + +```css +.header { + page-transition-tag: header; + page-transition-capture: container-and-child; +} + +.share-button { + page-transition-tag: share-button; +} + +/* Slide the roots from right to left */ +@keyframes slide-left { + to { transform: translateX(-100%); } +} + +::page-transition-root-outgoing { + animation-name: slide-left; +} + +::page-transition-root-incoming { + left: 100%; + animation-name: slide-left; +} + +/* Prevent the header content from stretching */ +::page-transition-image-outgoing(share-button), +::page-transition-image-incoming(share-button) { + object-fit: cover; +} +``` + +# Single-Page-App API + +The mechanism for cross-document transitions and SPA transitions involves the same phases, so an SPA API will expose those parts in the same page. + +Half-baked sketch: ```js -requestAnimationFrame(() => { - let pendingTransition = document.documentTransition.getPendingTransition(); - if (pendingTransition.getData().version !== 123) - return; - - // |offeredTransitionElements| provides a list of objects to access state - // saved from the old page. - let oldHeader = pendingTransition.offeredTransitionItems.get("header-id"); - if (oldHeader) { - // Add sharedid attribute to elements animated in the new DOM. - document.querySelector(".header").sharedid="header-id"; - - // Query the style information saved from the old page. - let oldHeaderStyle = oldHeader.getContainerComputedStyle(); - - // The pseudo elements for each shared element are associated with the root element. - // The existing [pseudoElement](https://drafts.csswg.org/web-animations-1/#dom-keyframeeffect-pseudoelement) option can be used - // to target them with Web Animations API. - document.documentElement.animate( - [{ width: oldHeaderStyle.width, - height: oldHeaderStyle.height, - transform: oldHeaderStyle.transform }], - { duration: 1000, - pseudoElement: '::shared-container(header-id)' }); - } +document.documentTransition.prepare(async (transition) => { + // …Identify State-A elements to capture, then: + await updatePageStateSomehow(); + // The page is now in State-B. + // …Identify State-B elements to capture, then: + transition.start(); }); ``` -# Alternatives Considered - -## Heirarchical Properties - -This proposal disallows a shared element to be nested inside another shared -element. The restriction avoids the need to preserve the hierarchy of the shared -elements and associated properties (transform, clip, effects inherited by -descendents) when creating pseudo elements. This is a consideration for future -iterations of the feature. - -## Container/Child Split - -One consideration is to render each shared element using a replaced element -directly instead of creating a container element. The motivation behind this -split is to provide a stacking context to cross-fade the content of old and new -shared elements. This is necessary to ensure blending identical pixels is a -no-op using -[plus-lighter](https://drafts.fxtf.org/compositing/#porterduffcompositingoperators_plus_lighter) -blending. While same-origin transitions could work around this, it enables -future extensibility for cross-origin transitions where cross-fading identical -images would be common. - -## Natively Supporting Animating Box Decoration CSS Properties - -An alternate approach to the setup described in -[Animating Box Decoration CSS Properties](#live-animatable-properties) is to -support this natively in the browser by introducing a new `content-element()` -function. This function would behave similarly to the `element()` function -except skipping the following properties when painting the element: box -decorations and visual effects which generate a stacking context. The image -would also be sized to the element's content-box (as opposed to the border-box -used by the element() function). The motivation for supporting this natively -would be to make these properties animatable instead of requiring developers to -implement it themselves. +# Cross-fading + +Cross-fading two DOM elements is currently impossible if both layers feature transparency. This is due to the default composition operation: black with 50% opacity layered over black with 50% opacity becomes black with 75% opacity. + +However, the [plus-lighter](https://drafts.fxtf.org/compositing/#porterduffcompositingoperators_plus_lighter) compositing operation does the right thing when isolated to a set of elements whose `opacity` values add to 1. The "image wrapper" is meant to provide this isolation. + +Allowing `mix-blend-mode` to be set to `plus-lighter` will enable developers to create real cross-fades between elements for this feature and elsewhere. + +# Relation to `element()` + +CSS has an [`element()`](https://drafts.csswg.org/css-images-4/#element-notation) feature which allows the appearance of an element to be used as an image. + +This doesn't quite match either of the cases where we need to capture an element as an image. + +When capturing 'as a single image', it seems much easier for developers if we expand the capture to include things outside the border box, such as box shadows. `element()` clips at the border box. + +When capturing as a 'computed style + content image', we capture the combination of the element's children as image, clipped to the content box. Again this is different to `element()`. + +However, these variations could be included in `element()` using modifiers or similarly named functions (e.g. `element-children()`). [Here](https://jsbin.com/bisoleziyi/edit?html,output) is a polyfill example of a single image mode transition built using the existing element() support in Firefox. # Security/Privacy Considerations -The security considerations below cover same-origin transitions. These are a -subset of what's required for cross-origin transitions : - -* Script can never read pixel content for images generated using the element() - function. This is necessary since the document may embed cross-origin - content (iframes, CORS resources, etc.) and multiple restricted user - information (visited links history, dictionary used for spell check, etc.) -* The Live Animatable Properties could reference resources which are - restricted in the new document for an MPA navigation. For example, the old - page may use a cross-origin image for border-image which can't be accessed - by the new page due to differences in - [COEP](https://wicg.github.io/cross-origin-embedder-policy/). Fetching these - styles will fail on the new page. For same-origin navigations, the developer - already has knowledge of the cross-origin policy on the new page. They can - ensure not to reference cross-origin resources in the properties made live. - -# Related Reading - -An aspect of the feature that needs to be defined is the -[type of navigations](https://github.com/WICG/app-history#appendix-types-of-navigations) -that the old page can configure. We expect this will closely align with the -navigations that can be observed by the page using app-history's -[navigate event](https://github.com/WICG/app-history#restrictions-on-firing-canceling-and-responding). - -[^1]: The pseudo elements in the top layer will not have an associated - [::backdrop](https://fullscreen.spec.whatwg.org/#::backdrop-pseudo-element) - that is created for other elements in the top layer. +The security considerations below cover same-origin transitions. + +* Script can never read pixel content in the images. This is necessary since the document may embed cross-origin content (iframes, CORS resources, etc.) and multiple restricted user information (visited links history, dictionary used for spell check, etc.) +* If an element is captured as a 'computed style + content image', any external resources specified on the container, such as background images, will be re-fetched in the context of the new page to account for differences in sandboxing. + +Cross-origin transitions aren't yet defined, but are likely to be heavily restricted. + +# Interactivity and accessibility + +Page transitions are a purely visual affordance. In terms of interactivity, transition elements will behave like `div`s regardless of the original element. + +Developers could break this intent by adding interactivity directly to the transition element, e.g. by deliberately adding a `tabindex` attribute. But this isn't recommended. + +The page transition stage will be hidden from assistive technologies such as screen readers. + +- [Open question](https://github.com/WICG/shared-element-transitions/issues/88): Should hit-testing ignore transition elements? diff --git a/feature_details.md b/feature_details.md deleted file mode 100644 index 3b109fc..0000000 --- a/feature_details.md +++ /dev/null @@ -1,451 +0,0 @@ -# Shared Element Transitions - -This document is a detailed description of the Shared Element Transitions feature along with the design assumptions, constraints and targeted use-cases. The aim is to facilitate API discussion and ensure it's structured with a focus on interoperability. - -Note that since the API details are still in flux, this may not be consistent with other documentation. Please refer to the documentation in the [README](https://github.com/WICG/shared-element-transitions/blob/main/README.md) file, which is kept up to date with the latest details of the API. - -## Problem Statement - -When a user navigates on the web, they get to see the inner workings of web experiences: flash of white followed by a piece-meal rendering phase. This sequenced user experience results in a higher cognitive load because the user has to connect the dots between where they were, and where they are. Not only that, a smooth animation to transition between scenes also reduces the loading latency perceived by the user even if the actual loading time is the same. For these reasons, most platforms provide easy to use primitives to enable developers to build seamless transitions: [Android](https://developer.android.com/training/transitions/start-activity), [iOS/Mac](https://developer.apple.com/documentation/uikit/uimodaltransitionstyle) and [Windows](https://docs.microsoft.com/en-us/windows/apps/design/motion/page-transitions). - -This feature provides developers with the same capability on the web, irrespective of whether the scenes in a transition are rendered across Documents. The feature supports creating transitions between Documents which share the same [Browsing Context](https://developer.mozilla.org/en-US/docs/Glossary/Browsing_context). - -## Use Cases - -There are 3 categories of use-cases targeted by this feature which have different motivations and constraints: - -### Single Page Apps (SPA) - -The transitions targeted by this feature are in theory already possible for SPAs today but are complicated to get right. In order to achieve the same effect, the developer needs to do the following: - -* Retain the current version of the DOM -* Set up the new version of the DOM. This could be asynchronous if new content needs to be fetched. -* Set up animations between live DOM elements for both states -* Clean up the previous state at the end of the transition. - -Since live DOM needs to be present for both states at the same time, it's common to get the details wrong (eg, previous state might remain clickable or may stick around after the transition). This leads to difficult-to-debug issues and can have a negative impact on accessibility. - -We want to make authoring such transitions easy with a native API that offers some level of customization. - -### Multi Page Apps (MPA) - -It’s not possible to have these transitions today for multi-page apps (a.k.a. same-origin navigations). In fact, a lack of this ends up being one of the drivers for web authors to create SPAs. We want this API to enable authoring transitions within an MPA with a similar feature set as the SPA case above. - -### Cross Origin Transitions - -Cross-origin transitions are infeasible similar to MPAs. A common use-case for this is content aggregator sites (search engines, social media sites, news aggregators, etc.). These sites frequently display a hero image/header which could be animated during the navigation to provide a seamless transition. - -Cross-origin transitions have additional security/privacy constraints. In particular we must ensure the feature does not allow any cross-site information sharing which imposes the following restrictions: - -* The animations associated with the transition need to be defined completely by the previous Document, which is aware of the site the user is navigating to and has contextual information about the user journey. - -* The animations need to be executed such that there are no observable side-effects for the new and previous Documents. For example, setting an obscure animation for a specific element on the incoming Document could be used to exchange information between origins. Enforcing no side-effects is an important consideration, since it can influence the stage in the rendering pipeline where the UA executes these animations. - -An important assumption in the design of this feature is that it's reasonable to limit customization options for cross-origin transitions to make it easier for UAs to enforce the constraints above. But the API/implementation should strive to not unnecessarily limit capabilities for same-origin transitions due to cross-origin constraints. - -## Glossary - -| Term | Description | -| ------------- | ------------- | -| Previous Document | The Document the user is viewing when a navigation is initated. For the SPA case, this is effectively the old version of the DOM. | -| New Document | The Document which will be current in the session history when a navigation is committed. For the SPA case, this is effectively the new version of the DOM. | -| Root transition | An animation to transition the content for root element of both Documents. | -| Exit transition | An animation which specifies how an element in the previous Document exits the screen. | -| Enter transition | An animation which specifies how an element in the new Document enters the screen. | -| Snapshot | A pixel copy for a DOM subtree. | - -## Sample Code - -This section explains how the feature can be used to add transitions to MPAs. There are a set of use-cases and edge cases to consider : - -### Root Transition - -Let's start with a root transition, which adds an animation to the root element of previous and new Documents. Below is a video of the transition we want to create: - -[![Video Link for Root Element Transition](https://img.youtube.com/vi/j1EybZbfG5g/0.jpg)](https://www.youtube.com/watch?v=j1EybZbfG5g) - -The entry point for this API is a new `document.documentTransition` object. The previous Document provides a list of elements which will be animated during the transition. Since this is root transition, this list only includes the `html` element. The result of calling `documentTransition.setSameOriginTransitionElements` as illustrated below is that the browser caches a copy of the pixels (snapshot) for the `html` element when a navigation to `foo.com/a.html` is initiated. - -```js -// The list of elements is keyed on url. When a user navigates to -// a url, the corresponding entry in this dictionary is used to -// decide which elements should be cached. -document.documentTransition.setSameOriginTransitionElements( - { - “foo.com/a.html” : [{element : document.html}], - }); -``` - -Now we need to add some code on the new Document. When the new Document loads, script registers a listener for a new event type called `handleTransition`. The event is dispatched if a transition was initiated by the previous Document. The exact Document lifecycle stage before which this event should be registered is TBD. - -```js -document.documentTransition.addEventListener( - “handleTransition”, e => { - e.startTransition(provideTransitionFrom(e.previousUrl, e.previousElements)); - }); - -async function provideTransitionFrom(previousUrl, previousElements) { - // Wait for the image on the new page to load before - // starting the transition. - await waitForImageLoad(); - - // Old content slides with a fade out from center to left. - let oldRootAnimation = { - previousElement: previousElements[0], - transitionType: "slide-fade-left" - }; - - // New content is drawn underneath, revealed as the old - // content slides. - let newRootAnimation = { - newElement: document.html, - transitionType: "none" - }; - - return [oldRootAnimation, newRootAnimation]; -} -``` - -Adding an event listener for `handleTransition` will result in execution of following steps: - -* The browser keeps displaying content for the previous Document when `handleTransition` is dispatched. This does not require the previous Document to be active since the browser can display a cached pixel copy. - -* * The `handleTransition` event has a field `previousURL`: the URL for the previous Document. And `previousElements`: the list of placeholders for elements passed to `setSameOriginTransitionElements`, clarified in [API Proposal](#api-proposal). - -* The `handleTransition` event has an API `startTransition` which takes a promise to allow script to asynchronously prepare the new Document for first paint. The browser continues to display old content until the promise passed to this API resolves. On resolution, this promise provides a list of elements to animate and the type of animation. In the example above, `provideTransitionFrom` returns the root elements for the previous and new Document. Also note that the animation done is based on `transitionType` specified with each element. This is a new enum with a pre-defined list of animation patterns. - -### Single Element Transition - -We can also set a separate animation for parts of the page by specifying them separately in the API. Let's say we wanted the header to slide up during this transition: - -*TODO : Add rough implementation for video.* - -Add the elements which will animate independently in the API call on the previous Document. The result is that the browser caches a separate pixel copy for each element in the list. - -```js -document.documentTransition.setSameOriginTransitionElements( - { - “foo.com/a.html” : [{element : document.html}, - {element : document.getElementById("header")}], - }); -``` - -The `handleTransition` event on the new Document specifies the animation for the header as follows: - -```js -document.documentTransition.addEventListener("handleTransition", (event) => { - event.startTransition( - provideTransitionFrom(event.previousUrl, event.previousElements) - ); -}); - -async function provideTransitionFrom(previousUrl, previousElements) { - // … - let headerAnimation = { - previousElement: previousElements[1], - transitionType: "slide-up", - }; - return [oldRootAnimation, newRootAnimation, headerAnimation]; -} - -``` - -### Paired Element Transition - -In some cases a semantically or visually same element is present in both Documents. We can set up an animation which automatically animates this element from it's old state to the new state: - -[![Video Link for Shared Element Transition](https://img.youtube.com/vi/n9zxarKTpQ8/0.jpg)](https://www.youtube.com/watch?v=n9zxarKTpQ8) - -Similar to the case above, update the list in `setSameOriginTransitionElements` to include the animated image: - -```js -document.documentTransition.setSameOriginTransitionElements({ - "foo.com/a.html": [ - { element: document.html }, - { element: document.getElementById("dog") }, - ], -}); -``` - -And specify a transition which associates these elements in the `handleTransition` event on the new Document: - -```js -document.documentTransition.addEventListener("handleTransition", (event) => { - event.startTransition( - provideTransitionFrom(event.previousUrl, event.previousElements) - ); -}); - -async function provideTransitionFrom(previousUrl, previousElements) { - await waitForImageLoad(); - - // … - - let imageAnimation = { - previousElement: previousElements[1], - newElement: document.getElementById("dog"), - }; - return [oldRootAnimation, newRootAnimation, imageAnimation]; -} -``` - -### Network Latency - -Performing a seamless transition requires deferring first paint until the new Document is ready for display. Web authors should ensure that this delay is reasonable and gracefully handle slow network activity. A potential way to do this could be to update the transition based on whether a resource could be fetched within a deadline. Let's take the example of [Paired Element Transition](#paired-element-transition) again: - -```js -async function provideTransitionFrom(previousUrl, previousElements) { - // … - - const imageLoaded = await waitForImageLoadWithTimeout(); - - // Specify a paired transition if the image could be loaded within - // a deadline. - if (imageLoaded) { - let imageAnimation = { - previousElement: previousElements[1], - newElement: document.getElementById("dog"), - }; - return [oldRootAnimation, newRootAnimation, imageAnimation]; - } - - // Otherwise fallback to a simpler root transition. - return [oldRootAnimation, newRootAnimation]; -} -``` - -### New Document Layout - -A transition may need to be modified based on the layout of the new document for a given viewport size. Let's pick up the paired transition example again with a smaller browser window, which doesn't look as nice when the new paired element is partially onscreen: - -[![Video Link for Shared Element Offscreen Transition](https://img.youtube.com/vi/uBtDyw_2QVw/0.jpg)](https://www.youtube.com/watch?v=uBtDyw_2QVw) - -```js -async function provideTransitionFrom(previousUrl, previousElements) { - // … - - let elem = document.getElementById("dog"); - let visibleRect = elem.getBoundingClientRect(); - - // Only use a paired transition if at least half the element in - // either direction is onscreen. - if ( - visibleRect.width > elem.width / 2 || - visibleRect.height > elem.height / 2 - ) { - let imageAnimation = { - previousElement: previousElements[1], - newElement: elem, - }; - - return [oldRootAnimation, newRootAnimation, imageAnimation]; - } - - // Otherwise fallback to a simpler root transition. - return [oldRootAnimation, newRootAnimation]; -} -``` - -### Old Document State - -Customizing a transition may depend on state from the old Document. The feature enables this by letting the web author pass contextual information between Documents. The following example considers a case where we use the source of navigation to decide the transition: - -[![Video Link for Old Dom State](https://img.youtube.com/vi/6eGmjn0o_3Q/0.jpg)](https://www.youtube.com/watch?v=6eGmjn0o_3Q) - -The old Document passes info about which button click initiated the navigation: - -```js -document.documentTransition.setSameOriginTransitionElements({ - "foo.com/one.html": [ - { - element: document.getElementById("dog"), - data: "previous", - }, - ], - "foo.com/three.html": [ - { element: document.getElementById("dog"), data: "next" }, - ], - "foo.com/fifty.html": [ - { - element: document.getElementById("dog"), - data: "random", - }, - ], -}); -``` - -The new Document decides the transition type based on this information: - -```js -async function provideTransitionFrom(previousUrl, previousElements) { - // … - let type = "none"; - switch (previousElements[0].getData().type) { - // Old image slides to the left and fades away. - case "previous": - type = "slide-fade-left"; - break; - // Old image slides to the right and fades away. - case "next": - type = "slide-fade-right"; - break; - // Old image scales to grow in size and fades away. - case "random": - type = "explode"; - break; - } - - // Specify an animation for the old image to exit revealing new - // image. - let imageAnimation = { - previousElement: previousElements[1], - transitionType: type, - }; - return [imageAnimation]; -} -``` - -### Integration with App History API - -This feature integrates well with the [app-history API](https://github.com/WICG/app-history) which provides developers with a central framework for all navigation related logic. The following code-snippet provides an example: - -```js -appHistory.addEventListener("navigate", (event) => { - if (isSameOrigin(event.destination)) { - document.documentTransition.setSameOriginTransitionElements({ - [event.destination.url]: [{ element: document.html }], - }); - } -}); -``` - -Transition handling in the new Document remains the same. - -## API Proposal - -The proposal below is roughly divided into 3 parts: the APIs used on the old and new Document for a same origin cross-document navigation and the API for a transition within the same Document. - -### Previous Document - -The first part of the API is a dictionary: `CacheElement`. This is used to specify elements to cache from the previous Document. This dictionary has the following parameters: - -* `element`: A reference to the [Element](https://dom.spec.whatwg.org/#interface-element). *TODO : What should we do if layout for this element is disabled when a navigation is initiated? Also need to define how the element is clipped when snapshotted (local, overflow and ancestor clipping).* - -* `data`: Opaque data provided by the developer to pass any opaque contextual information to the new Document. The navigation trigger in [Old Document State](#old-document-state) is an example of such data. Allows any javascript object supported by the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) to provide maximum flexibility. The reason for limiting to data cloneable data types is to permit serialization of these objects, if necessary. - -The syntax for `documentTransition.setSameOriginTransitionElements` is : - -```js -document.documentTransition.setSameOriginTransitionElements({ - // nextURL: List of elements to cache. - USVString: [CacheElement, CacheElement], - USVString: [CacheElement, CacheElement], - // … -}); -``` - -Special handling is needed for a list of elements : `[CacheElement_Parent, CacheElement_Child]` where `CacheElement_Child` is a descendant of `CacheElement_Parent`. The parent element's snapshot excludes content for any descendant elements. The space covered by the descendant element shows the background which was earlier occluded by the descendant element. - -*TODO: Clarify the syntax for nextURL. This should allow for [URL patterns](https://github.com/WICG/urlpattern).* - -### New Document - -#### Referencing Elements - -The first concept for this feature on the new Document is a class: `PreviousDocumentElement`. This class provides a placeholder used by script in the new Document to reference elements from the previous Document. A `PreviousDocumentElement` object in the new Document maps 1:1 to a `CacheElement` object in the previous Document. This class has a `getData()` API to retreive the `data` for the corresponding `CacheElement`. - -#### Animation Configuration - -The Shared Element Transition feature allows the web author to compose a transition by animating a set of elements. For example, animating the roots and sub-elements in [Single Element Transition](#single-element-transition) and [Paired Element Transition](#paired-element-transition). We divide these animations into the following semantic transition types: - -* Single Element Transition: An element which exists only on the new Document animates as it enters the screen. An element which exists only on the previous Document animates as it exits the screen. A Root transition can be built using two Single Element Transitions. - -* Paired Element Transition: An element which exists on both Documents is automatically animated between them. These elements may or may not be visually identical. - -The parameters to configure an instance of Single Element or Paired Element Transition are specified using a new dictionary type : `ElementTransition`. This dictionary has the following fields: - -* `transitionType`: This is an enum to provide a predefined set of animation patterns for Single Element transitions. These can be explicit patterns like `slide-left` (does a translation in the specified direction) or higher level abstractions like `previous`/`next` which allow the UA to decide the appropriate animation (for example based on the idiomatic pattern for the native OS). The exact types are TBD. - -* `duration`: The total length for this transition. The browser may cap this to a reasonable limit. - -* `delay`: A Document transition establishes a universal timeline for multiple `ElementTransition`s. This field indicates the delay in starting animations for this transition on this universal timeline. - -* `newElement`: A reference to the [Element](https://dom.spec.whatwg.org/#interface-element) in the new Document. - -* `previousElement`: A reference to a `PreviousDocumentElement` provided in the `handleTransition` event (clarified below). - -If exactly one of `newElement` or `previousElement` is set, this is an enter or exit transition respectively. The exact animation itself is defined based on `transitionType`. If both `newElement` and `previousElement` are set, this is a paired transition with explicit start and end state. The details for element snapshots and how they are interpolated during the transition will be captured in a subsequent doc. TODO : Link when the doc is ready. - -### Transition Lifecycle - -The main entry point for the API in the new Document is the `handleTransition` [event](https://dom.spec.whatwg.org/#interface-event). This event is dispatched prior to first paint of a new Document if the previous Document had initialized a transition using `setSameOriginTransitionElements`. *TODO : Should this survive across redirects if the final URL is same origin?* Script can register for this event using `document.documentTransition` which is an [Event Target](https://dom.spec.whatwg.org/#interface-eventtarget). *TODO : Figure out the exact timing of this event in the Document's lifecycle. We also need to ensure that we allow script to register the event before first paint of the new Document.* - -The `handleTransition` event has the following fields : - -* `previousUrl`: The URL for the previous Document which initiated this transition. - -* `previousElements`: A list of `PreviousDocumentElement`s. These map 1:1 with the list of `CacheElement` passed to `setSameOriginTransitionElements`. - -This event also has an API to start the transition: `void startTransition(Promise>)`. The precise semantics of this API are described below: - -* The browser keeps displaying visuals for the previous Document until the `handleTransition` event is dispatched. If no call to `startTransition` is made by an [Event Listener](https://dom.spec.whatwg.org/#callbackdef-eventlistener), the transition is aborted. - -* The `startTransition` API takes a list of `ElementTransition`s. These transition objects specify the set of animations to execute for the transition. We use a promise based API here to allow the new Document to asynchronously prepare the new Document. Since a page load frequently involves activity like network fetches or disk reads, allowing the prepare to be asynchronous is important. - -* The browser continues to display visuals for the previous Document until the promise provided in `startTransition` resolves. The first rendering lifecycle update after this step marks the new Document's first paint and transition start. - -We also provide a `transitionEnd` event to be notified when all animations associated with a transition have finished executing. There should ideall be no visual changes to DOM state between the transition start step above and `transitionEnd` event. - - -### Single Page App API - -The API for same-document transitions (or SPAs) aligns very closely with the MPA API. The difference is in the API endpoints for each step described above. The following code-snippet shows a sample root transition: - -```js -async function doRootTransition() { - await document.documentTransition.performSameDocumentTransition({ - cacheElements: [{ element: document.html }], - startCallback: startTransition, - }); -} - -async function startTransition(previousElements) { - await waitForNextScene(); - let oldRootAnimation = { - previousElement: previousElements[0], - transitionType: "slide-fade-left", - }; - let newRootAnimation = { newElement: document.html, transitionType: "none" }; - return [oldRootAnimation, newRootAnimation]; -} -``` - -* The transition is initiated by calling the `performSameDocumentTransition` API. This API takes a list of `CacheElement` which should be cached by the browser for this transition. This is equivalent to initiating a navigation after issuing setSameOriginTransitionElements for a cross-document transition. - -* The `performSameDocumentTransition` API also takes a `startCallback` which is invoked by the browser once the step above to capture snapshots is finished. This is equivalent to the `handleTransition` event for cross-document transitions. - -* The `startCallback` receives a list of `PreviousDocumentElement`s. The semantics are similar to the `previousElements` field on `handleTransition` event. This callback returns a list of `ElementTransition`s to configure the animations to execute the transition. We use a promise based API here as well to allow asynchronous work for loading the next scene. The browser stops performing visual updates from DOM changes after invoking the `startCallback`. The updates are resumed when the promised returned by this callback resolves. This is equivalent to the `startTransition` API on `handleTransition` event. - -* The `performSameDocumentTransition` API returns a promise which is resolved once the associated transition is finished. This is equivalent to the `transitionEnd` event for cross-document transitions. - -## Open Questions - -* The type of navigations the previous Document will be allowed to define a transition for should be similar to the ones supported by app-history’s [navigate event](https://github.com/WICG/app-history#restrictions-on-firing-canceling-and-responding). The most likely exception will be back/forward buttons which currently do not fire a `navigate` event. So a common pattern would be to use this API in response to the navigate event. - -* Both cross-document and same-document transitions require deferring first paint for the new scene which is controlled by script. It's unclear what or if this should have an upper bound. - -* Since transitions execute with a live new Document, the state of new Elements can change while a transition is in progress. For example, an image element used in the transition receives more data or is detached from the DOM. It's unclear whether the behaviour here needs to be defined. - -## Security Considerations - -Since the design above is limited to same-origin transitions, the only security constraint is to ensure that script can never read pixel content for element snapshots. This is necessary since the Document may embed cross-origin content (iframes, CORS resources, etc.) and multiple restricted user information (visited links history, dictionary used for spell check, etc.) - -## Future Work - -Subsequent designs should cover the cross-origin use-case and address design considerations specific to that use-case. A few problems restricted to it are : - -* Opt-in vs Opt-out: The transition must be decided completely by the previous origin initiating the navigation. This necessitates the ability for the new origin to decide which origins can control a transition to it. It’s unclear whether this should be an opt-in or opt-out. The decision will need to tradeoff limiting the customizability of the transition to ensure there is no potential for abuse to allow an opt-out approach. - -* Referencing elements: Same-origin relies on sharing information across documents to reference elements between them, which won’t be an option for cross-origin transitions. This should leverage existing work for [scroll-to-css-selector](https://github.com/bryanmcquade/scroll-to-css-selector). - -* Time to First Paint: Same-origin relies on the new Document using an explicit signal to indicate when it is ready for display, which won’t be an option for cross-origin transitions. This will likely need to rely on UA heuristics to decide when the transition is started, especially with an opt-out approach. diff --git a/media/container-n-texture.mp4 b/media/container-n-texture.mp4 new file mode 100644 index 0000000..ca79904 Binary files /dev/null and b/media/container-n-texture.mp4 differ diff --git a/media/normal-nav.mp4 b/media/normal-nav.mp4 new file mode 100644 index 0000000..fdcfef1 Binary files /dev/null and b/media/normal-nav.mp4 differ diff --git a/media/page-a-parts.mp4 b/media/page-a-parts.mp4 new file mode 100644 index 0000000..c850cee Binary files /dev/null and b/media/page-a-parts.mp4 differ diff --git a/media/pages.png b/media/pages.png new file mode 100644 index 0000000..6f2837c Binary files /dev/null and b/media/pages.png differ diff --git a/media/single-texture.mp4 b/media/single-texture.mp4 new file mode 100644 index 0000000..d7bc9ab Binary files /dev/null and b/media/single-texture.mp4 differ diff --git a/media/transition.mp4 b/media/transition.mp4 new file mode 100644 index 0000000..2ee0777 Binary files /dev/null and b/media/transition.mp4 differ