Cross-Document Navigation Without a JavaScript Framework
Page changes in classic multi-page applications usually feel abrupt: the old page disappears and the new one appears with no transition at all. Cross-Document View Transitions close that gap directly in the browser, with a few lines of CSS, no single-page framework and no extra JavaScript for the basic case.
Table of Contents
- 1. What Cross-Document View Transitions actually solve
- 2. Enabling the @view-transition at-rule
- 3. Controlling named view transitions with view-transition-name
- 4. Transition types for different navigation directions
- 5. Custom animations with ::view-transition-old and -new
- 6. JavaScript hooks: pageswap and pagereveal
- 7. Fallback strategies for unsupporting browsers
- 8. Performance and layout shift during page transitions
- 9. Cross-Document View Transitions compared
- 10. Summary
- 11. FAQ
1. What Cross-Document View Transitions actually solve
Classic multi-page applications load a completely new document on every navigation. The browser discards the old page, reserves a blank intermediate state, and renders the new page from scratch. This exact hard cut is what View Transitions between pages remove: instead of a flash, the browser shows an animated crossfade between the last visible state of the old page and the first visible state of the new page. Users get the impression of a continuous application, even though two separate HTML documents were technically loaded.
Before Cross-Document View Transitions were available, achieving this effect required considerable effort: either a full single-page architecture with client-side routing, or workarounds using iframes and manual state management. Both paths add extra complexity, more JavaScript and a larger surface for bugs. With the native View Transition API for cross-document navigation, the architecture stays completely unchanged: normal links, normal form submissions, normal server responses. The animation lives entirely in the presentation layer.
This is especially valuable for content-driven projects such as blogs, shops or documentation sites that have good reasons (SEO, server rendering, robustness) not to adopt an SPA architecture. Cross-Document View Transitions deliver exactly the transition quality otherwise known only from JavaScript frameworks, without their bundle size and without their router complexity.
2. Enabling the @view-transition at-rule
Getting started with Cross-Document View Transitions is deliberately simple. A single at-rule in the stylesheet of both involved pages is enough: @view-transition { navigation: auto; }. This rule tells the browser that navigations within the same origin should be treated as a view transition instead of a hard page swap. If the rule is missing on the target page, no transition happens, even if the starting page declared it.
Important in practice: navigation: auto only applies to same-origin navigations and only to normal top-level document changes, meaning link clicks or form submits, not reloads or external links. For multi-page apps with many templates it pays off to place the at-rule in a shared base stylesheet, so every new page automatically participates in transitions without developers having to remember it per template.
/* base.css — shared across all pages of the site */
@view-transition {
navigation: auto;
}
/* Optional: disable transitions for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
Without any further adjustments, the browser already provides a default crossfade animation: the old page fades out, the new page fades in, both slightly overlapping. That is the baseline of Cross-Document View Transitions, and for many projects already a noticeable improvement over a hard refresh. The real strength of the View Transition API only shows once individual elements are animated on their own.
3. Controlling named view transitions with view-transition-name
For a specific element to be treated as its own animated object across the page boundary, it gets a unique name: view-transition-name: product-image;. If the browser finds an element with the same name on the target page, it automatically creates a morph animation between the position, size and appearance of the old and new element. That is the core of what is commonly known as a shared element transition, now natively available for cross-document navigation without any JavaScript library.
A typical example is a product list navigating to a product detail page. The thumbnail in the list gets the same view-transition-name as the large image on the detail page. On click, the browser visibly glides the image from the small position to the large position, including the size change, instead of simply making it disappear and reappear. Each name must only be assigned once per page, otherwise the browser aborts the transition for that element.
/* Product list page */
.product-card img {
view-transition-name: var(--vt-name);
}
/* Assign a unique name per product via inline custom property */
/* <img style="--vt-name: product-42" ...> */
/* Product detail page — same name on the hero image */
.product-hero img {
view-transition-name: var(--vt-name);
}
/* Custom crossfade duration for the named group */
::view-transition-group(product-42) {
animation-duration: 0.4s;
}
For dynamic names, for example in a list with many products, the name is usually set via a CSS custom property that gets a different value per element, either server side or directly in the markup. This keeps the CSS generic while every element receives its own unique view-transition-name at runtime. Without this technique, a separate CSS rule would be needed for every possible element, which is not practical with hundreds of products.
4. Transition types for different navigation directions
Not every navigation should look the same. A click on "next" in a gallery should feel different than a click on "back", and navigating to a parent page should feel different than navigating deeper into a structure. The View Transition API allows this through so-called transition types, which get set during navigation and can be queried in CSS with the :active-view-transition-type() pseudo-class.
In practice, a type is assigned on the triggering link, for example via document.startViewTransition({ types: ["forward"] }) in a small navigation script, or declaratively through the upcoming types attribute. In the stylesheet, the active type is then targeted directly: for "forward" the new page slides in from the right, for "back" from the left. This distinction creates a spatial sense of navigation that a plain crossfade cannot convey.
/* Direction-aware transitions via active transition type */
html:active-view-transition-type(forward) {
&::view-transition-old(root) {
animation: slide-out-left 0.3s ease-in both;
}
&::view-transition-new(root) {
animation: slide-in-right 0.3s ease-out both;
}
}
html:active-view-transition-type(back) {
&::view-transition-old(root) {
animation: slide-out-right 0.3s ease-in both;
}
&::view-transition-new(root) {
animation: slide-in-left 0.3s ease-out both;
}
}
@keyframes slide-in-right {
from { transform: translateX(100%); }
}
@keyframes slide-out-left {
to { transform: translateX(-100%); }
}
The effort for these direction-aware view transitions is manageable, yet the effect is clearly noticeable: users immediately recognize whether they are moving forward or backward through a structure. Especially with multi-step forms, wizard flows or image galleries, this investment pays off because spatial orientation directly contributes to the perceived quality of the application.
5. Custom animations with ::view-transition-old and -new
Every view transition internally builds a pseudo-element tree: ::view-transition-group as a container per named element, below it ::view-transition-image-pair, and inside that an ::view-transition-old and an ::view-transition-new snapshot. These pseudo-elements let you style every phase of the animation independently of the browser's default crossfade.
A common use case: instead of a simple fade, the old element should fly out upward while the new element comes in from below. This is achieved by setting a custom animation on ::view-transition-old(name) and a different one on ::view-transition-new(name). It is important to disable the default animation first, otherwise it overlays the custom animation and the result looks messy instead of controlled.
/* Custom animation for a specific named element */
::view-transition-old(headline) {
animation: 0.35s ease-in both fade-out-up;
}
::view-transition-new(headline) {
animation: 0.35s ease-out both fade-in-from-below;
}
@keyframes fade-out-up {
to { opacity: 0; transform: translateY(-24px); }
}
@keyframes fade-in-from-below {
from { opacity: 0; transform: translateY(24px); }
}
/* Group-level control: isolation avoids the default cross-fade double flash */
::view-transition-group(headline) {
animation-duration: 0.35s;
}
::view-transition-image-pair(headline) {
isolation: isolate;
}
A well-known pitfall is the double flash effect that appears when the old and new snapshots overlap while both are partially visible. Combining isolation: isolate on the image pair with a clearly defined sequence of animation phases fixes this reliably. Anyone building custom animations for view transitions should always inspect these pseudo-elements in the DevTools element panel, since they only appear in the DOM during an active transition.
6. JavaScript hooks: pageswap and pagereveal
Even though Cross-Document View Transitions work without any JavaScript at all, two events allow additional control: pageswap fires shortly before the old page is left, pagereveal fires shortly after the new page reaches its first render frame. Both provide access to the running transition object via event.viewTransition, including the ability to set transition types dynamically or abort the transition with skipTransition().
A practical scenario: you want to set the transition type based on the actual navigation direction in the DOM, for example based on a breadcrumb depth that is not easily known server side. In the pageswap handler this can be determined dynamically and attached to the active transition via event.viewTransition.types.add("forward") before it starts.
// Determine transition direction from a data attribute on the clicked link
window.addEventListener('pageswap', (event) => {
if (!event.viewTransition) return;
const direction = document.activeElement?.dataset?.navDirection;
if (direction === 'back') {
event.viewTransition.types.add('back');
} else {
event.viewTransition.types.add('forward');
}
});
// React once the new page has revealed its first frame
window.addEventListener('pagereveal', async (event) => {
if (!event.viewTransition) return;
await event.viewTransition.ready;
console.log('View transition to new page is now animating');
});
These events are deliberately optional. For most view transitions, plain CSS is enough. Only once application logic has to influence the transition decision, for example different animations depending on user role or A/B test variant, do pageswap and pagereveal become the right entry point, without needing a full router framework.
7. Fallback strategies for unsupporting browsers
Cross-Document View Transitions are progressive enhancement by definition. Browsers that do not know the @view-transition at-rule or view-transition-name simply ignore the declarations and show the classic, hard page swap instead. There is no crash, no missing functionality, just a less elegant transition. That is exactly why view transitions are so well suited for production systems: the feature can be rolled out immediately without waiting for full browser coverage.
Teams who still want consistent behavior across browser boundaries can add feature detection via @supports, combined with a CSS hint for developers about which code paths are actually active. More important in practice, however, is the combination with prefers-reduced-motion, since view transitions are explicitly a motion effect and should be disabled for users with the corresponding system setting, independent of the browser support level itself.
/* Feature detection: only opt in named transitions where supported */
@supports (view-transition-name: none) {
.product-card img {
view-transition-name: var(--vt-name);
}
}
/* Respect user motion preference regardless of browser support level */
@media (prefers-reduced-motion: reduce) {
@view-transition {
navigation: none;
}
}
In practice, the plain at-rule without extra feature detection is usually enough, because the base behavior degrades gracefully anyway. The prefers-reduced-motion rule, though, should be set in every project that uses Cross-Document View Transitions, because an automatic page transition that cannot be turned off is a real barrier for users with vestibular impairments.
8. Performance and layout shift during page transitions
An often underestimated aspect of view transitions is their impact on perceived and actual performance. For the duration of the transition, the browser keeps snapshots of the old and new page content, which briefly requires extra memory. On very large or image-heavy pages this can cause noticeable stutter on weaker devices, especially when many elements each carry their own view-transition-name.
The practical recommendation: only assign view-transition-name to elements that genuinely need a recognizable morph effect, not indiscriminately to every image tile in a list. The rest of the page is handled perfectly well by the default crossfade treatment of the root element. In addition, the load time of the target page itself must stay in view: a view transition does not mask a slow server, it only delays the visible reaction to the click by the animation duration, which can even extend the perceived waiting time on an already slow navigation.
An additional point concerns cumulative layout shift: if the new document loads fonts asynchronously or contains images without fixed dimensions, content shifts while the view transition is running, which immediately undoes the positive impression of the smooth transition. Fixed width/height attributes or aspect-ratio on images are therefore not a nice-to-have but a prerequisite for clean Cross-Document View Transitions.
9. Cross-Document View Transitions compared
To decide whether and how view transitions should be used in a project, a direct comparison of the available approaches for animated page transitions in classic multi-page architectures helps.
| Approach | Effort | JavaScript required | Assessment |
|---|---|---|---|
| Cross-Document View Transitions | Low | Optional | Native, progressive, ideal for MPA |
| SPA router with transition library | High | Mandatory | Full control, but architecture change |
| iframe based overlays | High | Mandatory | Fragile, SEO drawbacks |
| CSS fade via opacity class | Very low | Minimal | No real crossfade between documents |
| No transition (default) | None | None | Hard cut, feels less polished |
Cross-Document View Transitions win this comparison for the vast majority of classic multi-page projects, because they deliver the biggest effect for the smallest architectural effort. Only when an application already exists as a single-page app with its own router might a dedicated JavaScript transition library be the more consistent choice, because client-side routing is already part of that architecture.
Mironsoft
Modern CSS architecture and animation for Magento and Hyvä frontends
Page transitions that feel genuinely polished?
We integrate Cross-Document View Transitions into existing shop and content pages, including named transitions, a fallback strategy and a performance check for Core Web Vitals.
Concept
Analyze navigation flows and define suitable transition types
Implementation
@view-transition, named transitions and custom keyframe animations
Quality assurance
Fallback tests, reduced motion and layout shift control
10. Summary
Cross-Document View Transitions bring the transition quality known from single-page apps into classic multi-page architectures, without requiring a router or an additional framework. The at-rule @view-transition { navigation: auto; } enables the base functionality, view-transition-name creates shared element effects across page boundaries, and the pseudo-elements ::view-transition-old and ::view-transition-new allow fully custom animations instead of the default crossfade.
Three points matter for production use: a clear prefers-reduced-motion rule, a deliberate selection of elements carrying their own view-transition-name instead of assigning it indiscriminately, and fixed image dimensions to avoid layout shift during the transition. Because view transitions simply fall back to the classic page swap without support, the feature can be adopted risk free and expanded step by step, from a simple crossfade to fully choreographed, direction-aware navigation animations.
View Transitions Between Pages — Key Takeaways
Activation
@view-transition { navigation: auto; } on both involved pages, otherwise no transition occurs.
Named transitions
view-transition-name must be unique per page, creates morph effects between identically named elements.
Custom animation
::view-transition-old and ::view-transition-new replace the default crossfade with custom keyframes.
Safety
Progressive enhancement, no error without support. Always respect prefers-reduced-motion.