View Transitions Between Pages: Cross-Document Navigation Without a Framework
AI generated
{ }
@
CSS · View Transitions · Navigation · Multi-Page App
View Transitions Between Pages
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.

17 min read @view-transition · view-transition-name · pageswap · pagereveal Chrome 126+ · progressive enhancement

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.

11. FAQ: View Transitions Between Pages

1What is a Cross-Document View Transition?
An animated crossfade between two full HTML documents during navigation, without a single-page architecture.
2How do I enable view transitions?
@view-transition { navigation: auto; } on the starting and target page. If missing on either, the transition is skipped.
3What does view-transition-name do?
Marks an element as its own object that the browser morphs across the page boundary when the name matches on both pages.
4Two elements with the same name?
The transition for that element is aborted. Names must be unique per page, dynamic custom properties prevent collisions.
5Does this work in every browser?
Currently mostly Chromium. Without support the rule is ignored, the classic hard page swap remains, no error occurs.
6Animate forward and back differently?
Via transition types and :active-view-transition-type(), set in the pageswap event through event.viewTransition.types.add().
7Disabling for reduced motion?
@media (prefers-reduced-motion: reduce) { @view-transition { navigation: none; } } should be set in every project.
8Does this affect load time?
Not the loading itself, but the visible reaction by the animation duration. On slow servers, perceived waiting time increases.
9Avoiding layout shift during transition?
Set fixed image dimensions or aspect-ratio, preload web fonts. Otherwise content shifts during the running animation.
10Do I need JavaScript for this?
Not for the basic function. JavaScript is only needed when transition types must be determined dynamically from application logic.