Skip to content

Commit

Permalink
Merge pull request #208 from noamr/mpa-explainer
Browse files Browse the repository at this point in the history
Add explainer for cross-document navigations
  • Loading branch information
noamr committed May 29, 2023
2 parents 46d6a21 + 4fdf0c5 commit f4e3b48
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 110 deletions.
160 changes: 160 additions & 0 deletions cross-doc-explainer.md
@@ -0,0 +1,160 @@
# Contents

# Introduction

Cross-document transitions are an extension to
[same-document transitions](https://drafts.csswg.org/css-view-transitions-1/), adding the semantics
necessary to display transitions when navigating across documents.

## Scope
[The main explainer](explainer.md) and the [`css-view-transitions-1` spec](https://drafts.csswg.org/css-view-transitions-1/)
provide a detailed explanation about same-document view transitions. Most of that is applicable to
cross-document transitions as well. This document provides explanations about the additional
semantics, and how cross-document transitions work.

# Design Principles

## Compatible with same-document transitions

Developers shouldn't have to jump through hoops or rethink their transition design when switching
between an MPA architecture and an SPA architecture. The main building blocks of the transition,
the way the states are captured, the way the captured images are animated, and the JavaScript API, should remain the same, as much as possible.

## Declarative

Unlike same-origin transitions, Cross-document transitions should work automatically without JavaScript intervention. They should have the right CSS & JavaScript knobs for when the defaults are not enough.

## Same-origin (for now)

Cross-document view transitions are only enabled for
[same-origin](https://html.spec.whatwg.org/multipage/browsers.html#same-origin) navigations without a
[cross-origin redirect](https://html.spec.whatwg.org/#unloading-documents:was-created-via-cross-origin-redirects).
In the future we could examine relaxing this restriction in some way to same-site navigations.
Cross-site view transitions are a non-goal.

# How it works

## In a nutshell
Both the old and new document need to [declaratively opt-in](#declarative-opt-in) to the transition
between them. If both opted in, and this is a [same-origin](#same-origin) navigation without
cross-origin redirects, the state of the old document is captured, using the
[same algorithm](https://drafts.csswg.org/css-view-transitions-1/#capture-old-state-algorithm) used
for same-document transitions.

When the new document is about to present the first frame, i.e. when
the document is no longer [render blocked](https://html.spec.whatwg.org/multipage/dom.html#render-blocked)
or at the course of [reactivation](https://html.spec.whatwg.org/multipage/browsing-the-web.html#reactivate-a-document) from prerendering/back-forward cache, the state of the new document is captured, also using the
[equivalent algorithm](https://drafts.csswg.org/css-view-transitions-1/#capture-new-state-algorithm).

If all conditions are met and both states are captured, the transition proceeds to
[update the pseudo element styles](https://drafts.csswg.org/css-view-transitions-1/#update-pseudo-element-styles) and display the animation, as if it was a same-document transition.

The new document can customize the style of the animation using the same CSS techniques available
for same-document transitions. Both documents can interrupt the transition in different phases, or
observe its completion.

So to support cross-document view transition, the following things need to be specified:

1. A way for both documents to [opt in](#declarative-opt-in) to the transition.
1. The [lifecycle](#lifecycle): the exact moments in which the states are captured.
1. A [JavaSript API](#javascript-api) to control the animation, equivalent to how same-document
animations can be controlled.


## Declarative opt-in

To enable cross-document transitions, the old and new documents need to be coordinated with each
other - as in, the transition names in the old document match the ones in the new document and the
effect of animating between them is intentional. Otherwise, there could be a situation where two
documents of the same origin define styles for same-document transitions independently, and enabling
this feature would create an unexpected transition between them.

This is an issue that is specific to cross-document transitions, as same-document transitions are
triggered imperatively in the first place.

The minimal functional opt-in would be a global declaration that the document supports view
transitions, e.g.:

```html
<meta name="view-transitions" content="same-origin">
```

though to make this fully expressive, e.g. opt in conditionally based on reduced-motion preferences,
versions, or URL patterns, this would need a more elaborate definition, e.g.:

```css
@cross-document-view-transitions: allow;
@prefers-reduced-motion {
@cross-document-view-transitions: skip;
}
```

Note: The exact semantics of the conditional opt-in are TBD. See related open issues:
* w3c/csswg-drafts#8048
* w3c/csswg-drafts#8679
* w3c/csswg-drafts#8683

## Lifecycle

![Lifecycle chart for cross-document transitions](media/mpa-chart.svg)

### Capturing the old state

[The old state is captured](https://drafts.csswg.org/css-view-transitions-1/#capture-old-state-algorithm) right before the old document is hidden and a new one is shown.
In the HTML spec that moment is defined [here](https://html.spec.whatwg.org/#populating-a-session-history-entry:loading-a-document).
This can either happen during normal navigations, when the new document is about to be created,
in Back/Forward cache navigations, or when activating a prerendered document.

Before creating the new document (or activating a cached/prerendered one), the UA would [update the rendering](https://html.spec.whatwg.org/#update-the-rendering) and snapshot the old document, in the same manner a document is snapshotted for a same-document navigation.

The developer can use existing events like `navigate` (where available) or `click` to customize the
elements which have a view-transition-name in the old Document.

Example:
```js
navigation.addEventListener("navigate", event => {
// Don't capture navigation-bar animation when navigating to home
if (!new URL(event.destination.url).pathname.startsWith("/home"))
navigationBar.viewTransitionName = "none";
});
```

### Capturing the new state

The [new state is captured](https://drafts.csswg.org/css-view-transitions-1/#capture-new-state-algorithm) right before the first [rendering opportunity](https://html.spec.whatwg.org/#rendering-opportunity)
of the new document. This allows the new document to use the
[render-blocking mechanism](https://html.spec.whatwg.org/#render-blocking-mechanism) as a way to
delay the transition.

As shown in the chart above, that first rendering opportunity can come in two cases, either
it's a newly initialized document that's no longer [render-blocked](https://html.spec.whatwg.org/multipage/dom.html#render-blocked), or it's a document that's been frozen due to back-forward cache
or prerendered, and is now being activated.

Note: relying on the [render-blocking](https://html.spec.whatwg.org/multipage/dom.html#render-blocked) mechanism is limited, as only a limited set of elements participate in that
mechanism. See [proposal to extend it](https://github.com/whatwg/html/issues/9332).

## JavaScript API

To fullfill the design principle of making cross-document transitions [compatible with same-document transitions](#compatible-with-same-document-transitions), cross-document transition need to be as
customizable as same-document transitions, while allowing good defaults that work declaratively.

Same document transitions can be programatically extended (until the [updateCallbackDone](https://drafts.csswg.org/css-view-transitions-1/#dom-viewtransition-updatecallbackdone) promise is fullfilled), [skipped](https://drafts.csswg.org/css-view-transitions-1/#dom-viewtransition-skiptransition), programatically animated using the [ready](https://drafts.csswg.org/css-view-transitions-1/#dom-viewtransition-ready) promise, and their [end time can be
observed](https://drafts.csswg.org/css-view-transitions-1/#dom-viewtransition-finished).

To accomplish the same control and observability, the developer would need access to a
[ViewTransition](https://drafts.csswg.org/css-view-transitions-1/#the-domtransition-interface)
object. There are several ways to approach this, and this is an open issue. For example:

1. Expose it via `document.pendingViewTransition`. This would be available only before the document
gets render-unblocked for the first time, and right at reactivation.

1. Fire a special `reveal` event at both lifecycle moments, with a `ViewTransition` object. See [whatwg/html#9315](https://github.com/whatwg/html/issues/9315), w3c/csswg-drafts#8682, and w3c/csswg-drafts#8805.
Note that this event is different from [`pageshow`](https://html.spec.whatwg.org/#event-pageshow) as
in the newly initialized document `pageshow` is only fired once the document is fully loaded.

Note: skipping a transition on `pagehide` is guaranteed to happen before the new document is activated.

# Further discussions

See [the list of open issues labeled `css-view-transitions-2`](https://github.com/w3c/csswg-drafts/issues?q=css-view-transitions-2+label%3Acss-view-transitions-2) for the up-to-date list of issues, and w3c/csswg-drafts#8804 for the main discussion about
cross-document transitions.
112 changes: 2 additions & 110 deletions explainer.md
Expand Up @@ -11,7 +11,6 @@
1. [Transitioning elements don't need to exist in both states](#transitioning-elements-dont-need-to-exist-in-both-states).
1. [Customizing the transition based on the type of navigation](#customizing-the-transition-based-on-the-type-of-navigation) - e.g. creating 'reverse' transitions for 'back' traversals.
1. [Animating with JavaScript](#animating-with-javascript) - because some transitions aren't possible with CSS alone.
1. [Cross-document same-origin transitions](#cross-document-same-origin-transitions) - MPA page transitions.
1. [Compatibility with existing developer tooling](#compatibility-with-existing-developer-tooling).
1. [Compatibility with frameworks](#compatibility-with-frameworks).
1. [Error handling](#error-handling) - Ensuring DOM changes don't get lost, or stuck.
Expand Down Expand Up @@ -54,7 +53,7 @@ and [Windows](https://docs.microsoft.com/en-us/windows/apps/design/motion/page-t

# MPA vs SPA solutions

The [current spec](https://drafts.csswg.org/css-view-element-transitions-1/) and experimental implementation in Chrome (behind the `chrome://flags/#document-transition` flag) focuses on SPA transitions. However, the model has also been designed to work with cross-document navigations. The specifics for cross-document navigations are covered [later in this document](#cross-document-same-origin-transitions).
The [current spec](https://drafts.csswg.org/css-view-element-transitions-1/) and implementation in Chrome focuses on SPA transitions. However, the model has also been designed to work with cross-document navigations. The specifics for cross-document navigations are covered [here](cross-doc-explainer.md).

This doesn't mean we consider the MPA solution less important. In fact, [developers have made it clear that it's more important](https://twitter.com/jaffathecake/status/1405573749911560196). We have focused on SPAs due to the ease of prototyping, so those APIs have had more development. However, the overall model has been designed to work for MPAs, with a slightly different API around it.

Expand Down Expand Up @@ -351,114 +350,7 @@ https://user-images.githubusercontent.com/93594/184120371-678f58b3-d1f9-465b-978
# Cross-document same-origin transitions
This section outlines the navigation specific aspects of the ViewTransition API. The rendering model for generating snapshots and displaying them using a tree of targetable pseudo-elements is the same for both SPA/MPA.
## Declarative opt-in to transitions
The first step is to add a new meta tag to the old and new Documents. This tag indicates that the author wants to enable transitions for same-origin navigations to/from this Document.
```html
<meta name="view-transition" content="same-origin">
```
The above is equivalent to the browser implicitly executing the following script in the SPA API:
```js
document.startViewTransition(() => updateDOMToNewPage());
```
This results in a cross-fade between the 2 Documents from the default CSS set up by the browser. The transition executes only if this tag is present on both the old and new Documents. The tag must also be added before the body element is parsed.
The motivation for a declarative opt-in, instead of a script event, is:
* Enabling authors to define transitions with no script. If the transition doesn't need to be customized based on the old/new URL, it can be defined completely in CSS.
* Avoiding undue latency in the critical path for browser initiated navigations like back/forward. We want to avoid dispatch of a script event for each of these navigations.
Issue: The meta tag can be used to opt-in to other navigation types going forward: same-document, same-site, etc.
Issue: This prevents the declaration being controlled by media queries, which feels important for `prefers-reduced-motion`.
## Script events
Script can be used to customize a transition based on the URL of the old/new Document; or the current state of the Document when the transition is initiated. The Document could've been updated since first lold from user interaction.
### Script on old document
```js
document.addEventListener("crossdocumentviewtransitionoldcapture", (event) => {
// Cancel the transition (based on new URL) if needed.
if (shouldNotTransition(event.toURL)) {
event.preventDefault();
return;
}

// Set up names on elements based on the new URL.
if (shouldTagThumbnail(event.toURL)) {
thumbnail.style.viewTransitionName = "full-embed";
}

// Add opaque contextual information to share with the new Document.
// This must be [serializable object](https://developer.mozilla.org/en-US/docs/Glossary/Serializable_object).
event.setInfo(createTransitionInfo(event.toURL));
});
```
### Script on new document
```js
// This event must be registered before the `body` element is parsed.
document.addEventListener("crossdocumentviewtransition", (event) => {
// Cancel the transition (based on old URL) if needed.
if (shouldNotTransition(event.fromURL)) {
event.preventDefault();
return;
}

// The `ViewTransitionNavigation` object associated with this transition.
const transition = event.transition;

// Retrieve the context provided by the old Document.
const info = event.info;

// Add render-blocking resources to delay the first paint and transition
// start. This can be customized based on the old Document state when the
// transition was initiated.
markRenderBlockingResources(info);

// The `ready` promise resolves when the pseudo-elements have been generated
// and can be used to customize animations via script.
transition.ready.then(() => {
document.documentElement.animate(...,
{
// Specify which pseudo-element to animate
pseudoElement: "::view-transition-new(root)",
}
);

// Remove viewTransitionNames tied to this transition.
thumbnail.style.viewTransitionName = "none";
});

// The `finished` promise resolves when all animations for the transition are
// finished or cancelled and the pseudo-elements have been removed.
transition.finished.then(() => { ... });
});
```
This provides the same scripting points as the SPA API, allowing developers to set class names to tailor the animation to a particular type of navigation.
Issue: Event names are verbose. Bikeshedding needed.
Issue: Do we need better timing for `crossdocumentviewtransition` event? Especially for Documents restored from BFCache.
Issue: Customizing which resources are render-blocking in `crossdocumentviewtransition` requires it to be dispatched before parsing `body`, or explicitly allow render-blocking resources to be added until this event is dispatched.
Issue: We'd likely need an API for the developer to control how much Document needs to be fetched/parsed before the transition starts.
Issue: The browser defers painting the new Document until all render-blocked resources have been fetched or timed out. Do we need an explicit hook for when this is done or could the developer rely on existing `load` events to detect this? This would allow authors to add viewTransitionNames based on what the new Document's first paint would look like.
Issue: Since `crossdocumentviewtransitionoldcapture` is dispatched after redirects and only if the final URL is same-origin, it allows the current Document to know whether the navigation eventually ended up on a cross-origin page. This likely doesn't matter since the site could know this after the navigation anyway but knowing on the current page before the navigation commits is new.
This section has moved to [its own explainer](cross-doc-explainer.md).
# Compatibility with existing developer tooling
Expand Down
2 changes: 2 additions & 0 deletions media/mpa-chart.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit f4e3b48

Please sign in to comment.