Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add explainer for cross-document navigations #208

Merged
merged 24 commits into from May 29, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
155 changes: 155 additions & 0 deletions cross-doc-explainer.md
@@ -0,0 +1,155 @@
# 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, and the way the captured images are animated, should remain the
same, as much as possible.

## Declarative yes customizable
noamr marked this conversation as resolved.
Show resolved Hide resolved

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

## Same-origin

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.

# 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 carries on 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 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, and potentially
fire [new events](#a-new-reveal-event) that corresponds to those moments.
1. A way to [observe](#javascript-observability) or skip a transition using JavaScript in both
documents.


## 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 single-document transitions are
noamr marked this conversation as resolved.
Show resolved Hide resolved
triggered imperitavely in the first place.
noamr marked this conversation as resolved.
Show resolved Hide resolved

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
exit transition at moments that should be late enough.
noamr marked this conversation as resolved.
Show resolved Hide resolved

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind adding an example of this? The most common one would be naming one of the elements in the list whose details page the user is navigating to. Something like:

addeventlistener("navigate", (event) => {
  if (event.url == "item1)
    item1.viewTransitionName = "item";
});

### 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.

## JavaScript Observability

### `document.activeTransition`

In the same-document case, we gain access to a [`ViewTransition`](https://drafts.csswg.org/css-view-transitions-1/#viewtransition) object when we call [`startViewTransition()`](https://drafts.csswg.org/css-view-transitions-1/#ViewTransition-prepare). However, we don't have access to this
object for a declarative cross-document transition.

The proposal is to add a `document.activeViewTransition` property that would return an active `ViewTransition` object when there is an active transition, either cross-document or same-document.

This would allow skipping the transition and observing when it's finished.

Note: some of the properties of `ViewTransition`, like [`updateCallbackDone`](https://drafts.csswg.org/css-view-transitions-1/#dom-viewtransition-updatecallbackdone) are not relevant to cross-document
view transitions.

### A new (`reveal`?) event

Since the new state is [captured](#capturing-the-new-state) at a very precise point in time, we
might want to fire an event at that time, to let the new document make last-minute DOM changes
before the new state is captured, e.g. change transition names based on which images are loaded.

The new event would be fired at the first time the page ceases to be [render blocked](https://html.spec.whatwg.org/multipage/dom.html#render-blocked), and every time it is [reactivated](https://html.spec.whatwg.org/multipage/browsing-the-web.html#reactivate-a-document). Such event is

See whatwg/html#9315 and w3c/csswg-drafts#8805

Related Issue: Do we want to enable delaying the transition with JavaScript? Maybe with a timeout? See w3c/csswg-drafts#8681

# 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.
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
noamr marked this conversation as resolved.
Show resolved Hide resolved

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
noamr marked this conversation as resolved.
Show resolved Hide resolved
noamr marked this conversation as resolved.
Show resolved Hide resolved
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.