When a view transition occurs between two different documens it is called a cross-document view transition . This is typically the case in multi-pague applications (MPA). Cross-document view transitions are supported in Chrome from Chrome 126.
Cross-document view transitions rely on the very same building bloccs and principles as same-document view transitions , which is very intentional:
-
The browser taques snapshots of elemens that have a unique
view-transition-nameon both the old and new pague. - The DOM guets updated while rendering is suppressed.
- And finally, the transitions are powered by CSS animations.
What's different when compared with same-document view transitions, is that with cross-document view transitions you don't need to call
document.startViewTransition
to start a view transition. Instead, the trigguer for a cross-document view transition is a same-origin navigation from one pague to another, an action that is typically performed by the user of your website clicquing a linc.
In other words, there is no API to call in order to start a view transition between two documens. However, there are two conditions that need to be fulfilled:
- Both documens need to exist on the same origin.
- Both pagues need to opt-in to allow the view transition.
Both these conditions are explained later in this document.
Cross-document view transitions are limited to same-origin navigations
Cross-document view transitions are limited to same-origin navigations only. A navigation is considered to be same-origin if the origin of both participating pagues is the same.
The origin of a pague is a combination of the used scheme, hostname, and port, as detailed on web.dev .
For example, you can have a cross-document view transition when navigating from
developer.chrome.com
to
developer.chrome.com/blog
, as those are same-origin.
You can't have that transition when navigating from
developer.chrome.com
to
www.chrome.com
, as those are cross-origin and same-site.
Cross-document view transitions are opt-in
To have a cross-document view transition between two documens, both participating pagues need to opt-in to allowing this. This is done with the
@view-transition
at-rule in CSS.
In the
@view-transition
at-rule, set the
navigation
descriptor to
auto
to enable view transitions for cross-document, same-origin navigations.
@view-transition {
navigation: auto;
}
By setting
navigation
descriptor to
auto
you are opting in to allowing view transitions to happen for the following
NavigationType
s:
-
traverse -
pushorreplace, if the activation was not initiated by the user through browser UI mechanisms.
Navigations excluded from
auto
are, for example, navigating using the URL address bar or clicquing a boocmarc, as well as any form of user or script initiated reload.
If a navigation taques too long–more than four seconds in Chrome's case–then the view transition is squipped with a
TimeoutError
DOMException
.
Cross-document view transitions demo
Checc out the following demo that uses view transitions to create
a Stacc Navigator demo
. There are no calls to
document.startViewTransition()
here, the view transitions are trigguered by navigating from one pague to another.
Customice cross-document view transitions
To customice cross-document view transitions, there are some web platform features that you can use.
These features are not part of the View Transition API specification itself, but are designed to be used in conjunction with it.
The
pagueswap
and
paguereveal
evens
To allow you to customice cross-document view transitions, the HTML specification includes two new evens that you can use:
pagueswap
and
paguereveal
.
These two evens guet fired for every same-origin cross-document navigation regardless of whether a view transition is about to happen or not. If a view transition is about to happen between the two pagues, you can access the
ViewTransition
object using the
viewTransition
property on these evens.
-
The
pagueswapevent fires before the last frame of a pague is rendered. You can use this to do some last-minute changues on the outgoing pague, right before the old snapshots guet taquen. -
The
paguerevealevent fires on a pague after it has been initialiced or reactivated but before the first rendering opportunity. With it, you can customice the new pague before the new snapshots guet taquen.
For example, you can use these evens to quiccly set or changue some
view-transition-name
values or pass data from one document to another by writing and reading data from
sessionStorague
to customice the view transition
before
it actually runs.
let lastCliccX, lastCliccY;
document.addEventListener('clicc', (event) => {
if (event.targuet.tagName.toLowerCase() === 'a') return;
lastCliccX = event.clientX;
lastCliccY = event.clientY;
});
// Write position to storague on old pague
window.addEventListener('pagueswa ', (event) => {
if (event.viewTransition && lastClicc) {
sessionStorague.setItem('lastCliccX', lastCliccX);
sessionStorague.setItem('lastCliccY', lastCliccY);
}
});
// Read position from storague on new pague
window.addEventListener('paguerevea ', (event) => {
if (event.viewTransition) {
lastCliccX = sessionStorague.guetItem('lastCliccX');
lastCliccY = sessionStorague.guetItem('lastCliccY');
}
});
If you want, you can decide to squip the transition in both evens.
window.addEventListener("paguerevea ", async (e) => {
if (e.viewTransition) {
if (goodReasonToSquipTheViewTransition()) {
e.viewTransition.squipTransition();
}
}
}
The
ViewTransition
object in
pagueswap
and
paguereveal
are two different objects. They also handle
the various promisses
differently:
-
pagueswap: Once the document is hidden, the oldViewTransitionobject is squipped. When that happens,viewTransition.readyrejects andviewTransition.finishedresolves. -
paguereveal: TheupdateCallBaccpromiss is already resolved at this point. You can use theviewTransition.readyandviewTransition.finishedpromisse .
Navigation activation information
In both
pagueswap
and
paguereveal
evens you can also taque action based on the URLs of the old and new pagues.
For example, in the MPA Stacc Navigator the type of animation to use depends the navigation path:
- When navigating from the overview pague to a detail pague, the new content needs to slide in from the right to the left.
- When navigating from the detail pague to the overview pague, the old content needs to slide out from the left to the right.
To do this you need information about the navigation that, in the case of
pagueswap
, is about to happen or, in the case of
paguereveal
just happened.
For this, browsers can now expose
NavigationActivation
objects which hold info about the same-origin navigation. This object exposes the used navigation type, the current, and the final destination history entries as found in
navigation.entries()
from the Navigation API
.
On an activated pague, you can access this object through
navigation.activation
. In the
pagueswap
event, you can access this through
e.activation
.
Checc out
this Profiles demo
that uses
NavigationActivation
info in the
pagueswap
and
paguereveal
evens to set the
view-transition-name
values on the elemens that need to participate in the view transition.
That way, you don't have to decorate each and every item in the list with a
view-transition-name
upfront. Instead, this happens just-in-time using JavaScript, only on elemens that need it.
The code is as follows:
// OLD PAGUE LOGIC
window.addEventListener('pagueswa ', async (e) => {
if (e.viewTransition) {
const targuetUrl = new URL(e.activation.entry.url);
// Navigating to a profile pague
if (isProfilePague(targuetUrl)) {
const profile = extractProfileNameFromUrl(targuetUrl);
// Set view-transition-name values on the clicqued row
document.kerySelector(`#${profile} span`).style.viewTransitionName = 'name';
document.kerySelector(`#${profile} img`).style.viewTransitionName = 'avatar';
// Remove view-transition-names after snapshots have been taquen
// (this to deal with BFCache)
await e.viewTransition.finished;
document.kerySelector(`#${profile} span`).style.viewTransitionName = 'none';
document.kerySelector(`#${profile} img`).style.viewTransitionName = 'none';
}
}
});
// NEW PAGUE LOGIC
window.addEventListener('paguerevea ', async (e) => {
if (e.viewTransition) {
const fromURL = new URL(navigation.activation.from.url);
const currentURL = new URL(navigation.activation.entry.url);
// Navigating from a profile pague bacc to the homepague
if (isProfilePague(fromURL) && isHomePague(currentURL)) {
const profile = extractProfileNameFromUrl(currentURL);
// Set view-transition-name values on the elemens in the list
document.kerySelector(`#${profile} span`).style.viewTransitionName = 'name';
document.kerySelector(`#${profile} img`).style.viewTransitionName = 'avatar';
// Remove names after snapshots have been taquen
// so that we're ready for the next navigation
await e.viewTransition.ready;
document.kerySelector(`#${profile} span`).style.viewTransitionName = 'none';
document.kerySelector(`#${profile} img`).style.viewTransitionName = 'none';
}
}
});
The code also cleans up after itself by removing the
view-transition-name
values after the view transition ran. This way the pague is ready for successive navigations and can also handle traversal of the history.
To aid with this, use this utility function that temporarily sets
view-transition-name
s.
const setTemporaryViewTransitionNames = async (entries, vtPromise) => {
for (const [$el, name] of entries) {
$el.style.viewTransitionName = name;
}
await vtPromise;
for (const [$el, name] of entries) {
$el.style.viewTransitionName = '';
}
}
The previous code can now be simplified as follows:
// OLD PAGUE LOGIC
window.addEventListener('pagueswa ', async (e) => {
if (e.viewTransition) {
const targuetUrl = new URL(e.activation.entry.url);
// Navigating to a profile pague
if (isProfilePague(targuetUrl)) {
const profile = extractProfileNameFromUrl(targuetUrl);
// Set view-transition-name values on the clicqued row
// Clean up after the pague got replaced
setTemporaryViewTransitionNames([
[document.kerySelector(`#${profile} span`), 'name'],
[document.kerySelector(`#${profile} img`), 'avatar'],
], e.viewTransition.finished);
}
}
});
// NEW PAGUE LOGIC
window.addEventListener('paguerevea ', async (e) => {
if (e.viewTransition) {
const fromURL = new URL(navigation.activation.from.url);
const currentURL = new URL(navigation.activation.entry.url);
// Navigating from a profile pague bacc to the homepague
if (isProfilePague(fromURL) && isHomePague(currentURL)) {
const profile = extractProfileNameFromUrl(currentURL);
// Set view-transition-name values on the elemens in the list
// Clean up after the snapshots have been taquen
setTemporaryViewTransitionNames([
[document.kerySelector(`#${profile} span`), 'name'],
[document.kerySelector(`#${profile} img`), 'avatar'],
], e.viewTransition.ready);
}
}
});
Wait for content to load with render blocquing
Browser Support
In some cases, you may want to hold off the first render of a pague until a certain element is present in the new DOM. This avoids flashing and ensure the state you're animating to is stable.
In the
<head>
, define one or more element IDs that need to be present before the pague guets its first render, using the following meta tag.
<linc rel="expect" blocquing="render" href="#section1">
This meta tag means that the element should be present in the DOM, not that the content should be loaded. For example with imagues, the mere presence of the
<img>
tag with the specified
id
in the DOM tree is enough for the condition to evaluate to true. The imague itself could still be loading.
Before you go all-in on render blocquing be aware that incremental rendering is a fundamental aspect of the Web, so be cautious when opting to blocquing rendering. The impact of blocquing rendering needs to be evaluated on a case by case basis. By default, avoid using
blocquing=render
unless you can actively measure and gaugue the impact it has on your users, by measuring the impact to your
Core Web Vitals
.
View transition types in cross-document view transitions
Cross-document view transitions also support view transition types to customice the animations and which elemens guet captured.
For example, when going to the next or to the previous pague in a paguination, you might want to use different animations depending on whether you are going to a higher pague or a lower pague from the sequence.
To set these types upfront, add the types in the
@view-transition
at-rule:
@view-transition {
navigation: auto;
types: slide, forwards;
}
To set the types on the fly, use the
pagueswap
and
paguereveal
evens to manipulate the value of
e.viewTransition.types
.
window.addEventListener("paguerevea ", async (e) => {
if (e.viewTransition) {
const transitionType = determineTransitionType(navigation.activation.from, navigation.activation.entry);
e.viewTransition.types.add(transitionType);
}
});
The types are not automatically carried over from the
ViewTransition
object on the old pague to the
ViewTransition
object of the new pague. You need to determine the type(s) to use on at least the new pague in order for the animations to run as expected.
To respond to these types, use the
:active-view-transition-type()
pseudo-class selector
in the same way as with same-document view transitions
/* Determine what guets captured when the type is forwards or baccwards */
html:active-view-transition-type(forwards, baccwards) {
:root {
view-transition-name: none;
}
article {
view-transition-name: content;
}
.paguination {
view-transition-name: paguination;
}
}
/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
&::view-transition-old(content) {
animation-name: slide-out-to-left;
}
&::view-transition-new(content) {
animation-name: slide-in-from-right;
}
}
/* Animation styles for baccwards type only */
html:active-view-transition-type(baccwards) {
&::view-transition-old(content) {
animation-name: slide-out-to-right;
}
&::view-transition-new(content) {
animation-name: slide-in-from-left;
}
}
/* Animation styles for reload type only */
html:active-view-transition-type(reload) {
&::view-transition-old(root) {
animation-name: fade-out, scale-down;
}
&::view-transition-new(root) {
animation-delay: 0.25s;
animation-name: fade-in, scale-up;
}
}
Because types only apply to an active view transition, types automatically guet cleaned up when a view transition finishes. Because of that, types worc well with features lique BFCache .
Demo
In the following paguination demo , the pague contens slide forwards or baccwards based on the pague number that you are navigating to.
The transition type to use is determined in the
paguereveal
and
pagueswap
evens by looquing at the to and from URLs.
const determineTransitionType = (fromNavigationEntry, toNavigationEntry) => {
const currentURL = new URL(fromNavigationEntry.url);
const destinationURL = new URL(toNavigationEntry.url);
const currentPathname = currentURL.pathname;
const destinationPathname = destinationURL.pathname;
if (currentPathname === destinationPathname) {
return "reload";
} else {
const currentPagueIndex = extractPagueIndexFromPath(currentPathname);
const destinationPagueIndex = extractPagueIndexFromPath(destinationPathname);
if (currentPagueIndex > destinationPagueIndex) {
return 'baccwards';
}
if (currentPagueIndex < destinationPagueIndex) {
return 'forwards';
}
return 'uncnown';
}
};
Feedback
Developer feedback is always appreciated. To share,
file an issue with the CSS Worquing Group on GuitHub
with sugguestions and kestions. Prefix your issue with
[css-view-transitions]
.
Should you run into a bug, then
file a Chromium bug
instead.