Making the navigation experience customizable & declarative.
See whatwg/html#10616, whatwg/html#11819 and #12829.
The period between initiating a navigation (e.g. clicking a link) and consuming the content of the next page (FCP), is a sensitive moment in user experince. It is a point in time where users are very likely to notice delays, jarring moments of frozen display, and abrupt changes to presentation.
The core of this being difficult stems from a tradeoff between speed and smoothness:
- The new document becomes activate ASAP (in favor of speed), at the moment the headers are received.
- However, it cannot render until all of its render-blocking resources and elements are present (in favor of smoothness, preventing FoUCs).
So, in the cases where the navigation is not instant, providing some instant animated response might cause an experience that doesn't feel smooth.
This "uncanny valley" where the old page is no longer active but the new page is not rendering any frames is far from being an optimal user experience, and the knobs given for developers to control it are crude and implicit.
# User experience
The kind of UI authors commonly try to do with this approach is often referred to as "skeleton" - quickly transitioning to a UI that looks like the final state, but doesn't include any content, and then transitioning from the skeleton to the "real" UI that includes this content.
Other UIs include loading spinners, greying out the old page as it is unloading, or showing some directional swipe-like animation without displaying the target content.
This feature declaratively makes the navigation smoother. However, the view-transition starts very late. It captures the old state when the new page's response headers arrive, and then freezes the presentation until rendering is unblocked.
This would create an instant response to the navigation, however the animation would be interrupted as soon as the new page's headers arrive, freezing at that point. So by default this would be both abrupt and jarring.
This would feel smoother but slows down the whole navigation and tweaking it correctly is finicky.
This can reduce the jarring time, however it means the transition doesn’t end at the optimal state, and also doesn’t help with making it feel instant.
To make this part of the experience feel more seamless, developers should be able to create a "two-phase page transition". This transition starts instantly after navigation initiation (link click), and continues smoothly until the next page is ready to render. The instant part of the transition can only use information knows to the old page, which could be a skeleton of the new page or something generic of sorts.
To achieve that, proposing two provide the following:
// Returns a boolean if the page is prerendered/BFCached and not render-blocked.
navigateEvent.destination.ready
// Allows delaying the cross-document commit (aka page-swap) *without* intercepting this as a same-document navigation.
navigateEvent.deferPageSwap({
// "immediate", which is also the default, means that the history is swapped immediately, like a `pushState`,
// and the navigation becomes a `replace`.
historyChange: "immediate" | "after-transition",
// When the returned promise resovles, the navigation can proceed.
// The handler can register a "restore" callback, to be called if the navigation is aborted
// or if the page is restored from BFCache.
handler: (controller) => Promise
});Possible usage:
navigation.addEventListener("navigate", event => {
if (!event.destination.ready) {
event.deferPageSwap({
// This means that the history is swapped immediately, like a `pushState`,
// and the navigation becomes a `replace`.
historyChange: "immediate",
async handler(controller) {
const transition = document.startViewTransition(() => show_preview());
// The restore callback will be called if the navigation is aborted, or if this document is restored from BFCache.
controller.addRestoreCallback(() => hide_preview());
return transition.finished;
}
});
}
});- By default, the new history entry applies immediately, like a
pushStateorreplaceState. This makes it so that a quick press on "back" or so doesn't go too far back. ThehistoryChangeoption can opt out of this behavior. - Only same-origin navigations without cross-origin redirects are deferrable.
The above knobs can be very effective, but might also require expertise to get right.
Specifically, the addRestoreCallback mechanism can be easily overlooked by developers and cause bad UX when BFCache is enable and the state is not cleaned up properly.
As an alternative, proposing a declarative CSS-based solution, based on view-transition at rules, @navigation conditionals, and UA-provided view-transision types:
@navigation(phase: preview) {
#skeleton {
display: block;
}
}
@view-transition {
preview: always;
}
:active-view-transition-types(-ua-to-preview) {
.real-content .thumbnail {
view-transition-name: thumb-to-hero;
}
}
@navigation (phase: preview) {
.real-content .thumbnail {
view-transition-name: none;
}
:root:active-view-transition-types(-ua-to-preview) {
.skeleton .hero-placeholder {
view-transition-name: thumb-to-hero;
}
}
:root:active-view-transition-types(-ua-from-preview) {
& {
view-transition-name: none;
}
.skeleton {
view-transition-name: root;
}
}
}The @navigation (phase: preview) conditional allows authors to define styles that apply only during the preview phase of a cross-document view-transition,
while the two special "types" -ua-to-preview and -ua-from-preview can be used to define styles, specifically view-transition-names, when going from
the old real content to the preview state, or out from the skeleton to the new page.
While deferring page swap provides full flexibility, it also needs care to avoid some footguns that can cause unwanted navigation delays. However, the current plan is to enable that first for power users, and take learnings from that experience into the higher level CSS-based solution.
- What information does this feature expose, and for what purposes?
It may expose some information about timing of a navigation, including whether a prerendered page is ready. It is limited to same-origin navigations.
- Do features in your specification expose the minimum amount of information necessary to implement the intended functionality?
Yes
- Do the features in your specification expose personal information, personally-identifiable information (PII), or information derived from either?
No
- How do the features in your specification deal with sensitive information?
N/A
- Does data exposed by your specification carry related but distinct information that may not be obvious to users?
No
- Do the features in your specification introduce state that persists across browsing sessions?
No
- Do the features in your specification expose information about the underlying platform to origins?
No.
- Does this specification allow an origin to send data to the underlying platform?
No
- Do features in this specification enable access to device sensors?
No
-
Do features in this specification enable new script execution/loading mechanisms? No
-
Do features in this specification allow an origin to access other devices?
No
- Do features in this specification allow an origin some measure of control over a user agent's native UI?
Possibly as it changes the timing of a cross-document navigation. However this is already possible in various other ways, such as intercepting navigations or delaying them with a service worker.
- What temporary identifiers do the features in this specification create or expose to the web?
None
- How does this specification distinguish between behavior in first-party and third-party contexts?
It is only available for same-origin navigations.
- How do the features in this specification work in the context of a browser’s Private Browsing or Incognito mode?
N/A
- Does this specification have both "Security Considerations" and "Privacy Considerations" sections?
Yes
- Do features in your specification enable origins to downgrade default security protections?
No
- What happens when a document that uses your feature is kept alive in BFCache (instead of getting destroyed) after navigation, and potentially gets reused on future navigations back to the document?
This is handled specifically with the addRestoreCallback method.
- What happens when a document that uses your feature gets disconnected?
The navigation gets aborted anyway.
- Does your spec define when and how new kinds of errors should be raised?
Absolutely. It's a big part of the spec.
- Does your feature allow sites to learn about the user's use of assistive technology?
No
- What should this questionnaire have asked?
Nothing in particular.