When a view transition occurs between two different documents it is called a cross-document view transition. This is typically the case in multi-page applications (MPA). Cross-document view transitions are supported in Chrome from Chrome 126.
Browser Support
Cross-document view transitions rely on the very same building blocks and principles as same-document view transitions, which is very intentional:
- The browser takes snapshots of elements that have a unique
view-transition-name
on both the old and new page. - The DOM gets 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 trigger for a cross-document view transition is a same-origin navigation from one page to another, an action that is typically performed by the user of your website clicking a link.
In other words, there is no API to call in order to start a view transition between two documents. However, there are two conditions that need to be fulfilled:
- Both documents need to exist on the same origin.
- Both pages 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 pages is the same.
The origin of a page is a combination of the used scheme, hostname, and port, as detailed on web.dev.
![An example URL with the scheme, hostname, and port highlighted. Combined, they form the origin.](/https/developer.chrome.com/static/docs/web-platform/view-transitions/image/origin.png)
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 documents, both participating pages 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 NavigationTypes:
traverse
push
orreplace
, 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 clicking a bookmark, as well as any form of user or script initiated reload.
If a navigation takes too long–more than four seconds in Chrome's case–then the view transition is skipped with a TimeoutError
DOMException
.
Cross-document view transitions demo
Check out the following demo that uses view transitions to create a Stack Navigator demo. There are no calls to document.startViewTransition()
here, the view transitions are triggered by navigating from one page to another.
Customize cross-document view transitions
To customize 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 pageswap
and pagereveal
events
To allow you to customize cross-document view transitions, the HTML specification includes two new events that you can use: pageswap
and pagereveal
.
These two events get 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 pages, you can access the ViewTransition
object using the viewTransition
property on these events.
- The
pageswap
event fires before the last frame of a page is rendered. You can use this to do some last-minute changes on the outgoing page, right before the old snapshots get taken. - The
pagereveal
event fires on a page after it has been initialized or reactivated but before the first rendering opportunity. With it, you can customize the new page before the new snapshots get taken.
For example, you can use these events to quickly set or change some view-transition-name
values or pass data from one document to another by writing and reading data from sessionStorage
to customize the view transition before it actually runs.
let lastClickX, lastClickY;
document.addEventListener('click', (event) => {
if (event.target.tagName.toLowerCase() === 'a') return;
lastClickX = event.clientX;
lastClickY = event.clientY;
});
// Write position to storage on old page
window.addEventListener('pageswap', (event) => {
if (event.viewTransition && lastClick) {
sessionStorage.setItem('lastClickX', lastClickX);
sessionStorage.setItem('lastClickY', lastClickY);
}
});
// Read position from storage on new page
window.addEventListener('pagereveal', (event) => {
if (event.viewTransition) {
lastClickX = sessionStorage.getItem('lastClickX');
lastClickY = sessionStorage.getItem('lastClickY');
}
});
If you want, you can decide to skip the transition in both events.
window.addEventListener("pagereveal", async (e) => {
if (e.viewTransition) {
if (goodReasonToSkipTheViewTransition()) {
e.viewTransition.skipTransition();
}
}
}
The ViewTransition
object in pageswap
and pagereveal
are two different objects. They also handle the various promises differently:
pageswap
: Once the document is hidden, the oldViewTransition
object is skipped. When that happens,viewTransition.ready
rejects andviewTransition.finished
resolves.pagereveal
: TheupdateCallBack
promise is already resolved at this point. You can use theviewTransition.ready
andviewTransition.finished
promises.
Navigation activation information
In both pageswap
and pagereveal
events, you can also take action based on the URLs of the old and new pages.
For example, in the MPA Stack Navigator the type of animation to use depends the navigation path:
- When navigating from the overview page to a detail page, the new content needs to slide in from the right to the left.
- When navigating from the detail page to the overview page, 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 pageswap
, is about to happen or, in the case of pagereveal
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 page, you can access this object through navigation.activation
. In the pageswap
event, you can access this through e.activation
.
Check out this Profiles demo that uses NavigationActivation
info in the pageswap
and pagereveal
events to set the view-transition-name
values on the elements 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 elements that need it.
The code is as follows:
// OLD PAGE LOGIC
window.addEventListener('pageswap', async (e) => {
if (e.viewTransition) {
const targetUrl = new URL(e.activation.entry.url);
// Navigating to a profile page
if (isProfilePage(targetUrl)) {
const profile = extractProfileNameFromUrl(targetUrl);
// Set view-transition-name values on the clicked row
document.querySelector(`#${profile} span`).style.viewTransitionName = 'name';
document.querySelector(`#${profile} img`).style.viewTransitionName = 'avatar';
// Remove view-transition-names after snapshots have been taken
// (this to deal with BFCache)
await e.viewTransition.finished;
document.querySelector(`#${profile} span`).style.viewTransitionName = 'none';
document.querySelector(`#${profile} img`).style.viewTransitionName = 'none';
}
}
});
// NEW PAGE LOGIC
window.addEventListener('pagereveal', 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 page back to the homepage
if (isProfilePage(fromURL) && isHomePage(currentURL)) {
const profile = extractProfileNameFromUrl(currentURL);
// Set view-transition-name values on the elements in the list
document.querySelector(`#${profile} span`).style.viewTransitionName = 'name';
document.querySelector(`#${profile} img`).style.viewTransitionName = 'avatar';
// Remove names after snapshots have been taken
// so that we're ready for the next navigation
await e.viewTransition.ready;
document.querySelector(`#${profile} span`).style.viewTransitionName = 'none';
document.querySelector(`#${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 page 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 PAGE LOGIC
window.addEventListener('pageswap', async (e) => {
if (e.viewTransition) {
const targetUrl = new URL(e.activation.entry.url);
// Navigating to a profile page
if (isProfilePage(targetUrl)) {
const profile = extractProfileNameFromUrl(targetUrl);
// Set view-transition-name values on the clicked row
// Clean up after the page got replaced
setTemporaryViewTransitionNames([
[document.querySelector(`#${profile} span`), 'name'],
[document.querySelector(`#${profile} img`), 'avatar'],
], e.viewTransition.finished);
}
}
});
// NEW PAGE LOGIC
window.addEventListener('pagereveal', 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 page back to the homepage
if (isProfilePage(fromURL) && isHomePage(currentURL)) {
const profile = extractProfileNameFromUrl(currentURL);
// Set view-transition-name values on the elements in the list
// Clean up after the snapshots have been taken
setTemporaryViewTransitionNames([
[document.querySelector(`#${profile} span`), 'name'],
[document.querySelector(`#${profile} img`), 'avatar'],
], e.viewTransition.ready);
}
}
});
Wait for content to load with render blocking
Browser Support
In some cases, you may want to hold off the first render of a page 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 page gets its first render, using the following meta tag.
<link rel="expect" blocking="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 images, 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 image itself could still be loading.
Before you go all-in on render blocking be aware that incremental rendering is a fundamental aspect of the Web, so be cautious when opting to blocking rendering. The impact of blocking rendering needs to be evaluated on a case by case basis. By default, avoid using blocking=render
unless you can actively measure and gauge 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 customize the animations and which elements get captured.
For example, when going to the next or to the previous page in a pagination, you might want to use different animations depending on whether you are going to a higher page or a lower page 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 pageswap
and pagereveal
events to manipulate the value of e.viewTransition.types
.
window.addEventListener("pagereveal", 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 page to the ViewTransition
object of the new page. You need to determine the type(s) to use on at least the new page 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 gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
:root {
view-transition-name: none;
}
article {
view-transition-name: content;
}
.pagination {
view-transition-name: pagination;
}
}
/* 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 backwards type only */
html:active-view-transition-type(backwards) {
&::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 get cleaned up when a view transition finishes. Because of that, types work well with features like BFCache.
Demo
In the following pagination demo, the page contents slide forwards or backwards based on the page number that you are navigating to.
The transition type to use is determined in the pagereveal
and pageswap
events by looking 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 currentPageIndex = extractPageIndexFromPath(currentPathname);
const destinationPageIndex = extractPageIndexFromPath(destinationPathname);
if (currentPageIndex > destinationPageIndex) {
return 'backwards';
}
if (currentPageIndex < destinationPageIndex) {
return 'forwards';
}
return 'unknown';
}
};
Feedback
Developer feedback is always appreciated. To share, file an issue with the CSS Working Group on GitHub with suggestions and questions. Prefix your issue with [css-view-transitions]
.
Should you run into a bug, then file a Chromium bug instead.