WSS
BlogArchitecturePerformancePublished

View Transitions on MPAs: SPA-Feeling Navigation Without a Router

Same-document view transitions reached Baseline in October 2025 across Chrome, Firefox, and Safari. Cross-document transitions - the ones that make a real multi-page site feel like an SPA - are shipped in Chromium and reported inconsistently elsewhere; this article states what's actually verifiable and how to check the rest yourself before you ship.

The View Transitions API lets the browser animate between two DOM states - same document or, more relevantly for an MPA, two separate documents connected by an ordinary navigation - using native, GPU-composited animation. No client-side router. No framework transition library. The transition is a browser primitive triggered by a CSS at-rule and a navigation.

Same-document vs. cross-document: know which claim you’re relying on

These are two distinct features with distinct maturity, and treating them as one API with one support matrix is the most common error in coverage of this topic.

Same-document transitions (document.startViewTransition(), used within a single-page app or any script-driven DOM update) reached Baseline widely available status in October 2025, with Firefox 144 completing the set alongside existing Chrome and Safari support. This is settled.

Cross-document transitions - navigation between two separate HTML documents, which is the case that actually matters for an MPA - are less settled, and coverage of exactly how unsettled is inconsistent enough that I’m going to show you the disagreement rather than pick a side of it.

MDN states cross-document transitions require both documents to opt in via a @view-transition at-rule and same-origin navigation, without giving a full per-browser matrix in the primary article. A detailed compatibility reference reports Chrome shipping cross-document transitions from Chrome 126, and states plainly that Safari has not shipped cross-document view transitions as of its last update. A separate, differently-dated source reports Safari following in 18.2. A third source frames Firefox cross-document support as “expected in 2026” without confirming a ship date.

I’m not resolving that conflict for you, because I can’t verify it well enough to be confident, and stating a false certainty here is worse than admitting the uncertainty. What I can tell you with confidence: cross-document transitions are shipped and reliable in Chromium (Chrome 126+, Edge). Their status in Safari and Firefox needs to be checked against caniuse or tested directly in your target browsers on the day you ship, not assumed from any single article - including this one, six months from now.

/* Test for support directly rather than trusting a support table. */
@supports (view-transition-name: none) {
  /* cross-document transition CSS here */
}

Same-document transitions: the settled, safe-to-use case

function updateContent(newHTML) {
  if (!document.startViewTransition) {
    // No support: DOM updates, no animation. Fully functional, just not animated.
    container.innerHTML = newHTML;
    return;
  }
  document.startViewTransition(() => {
    container.innerHTML = newHTML;
  });
}

The browser snapshots the before and after states, then plays a default cross-fade between them. Nothing about this requires a full page navigation - it’s equally applicable inside a genuinely stateful application, which is a fair use case for a view transition regardless of your position on client-side routing generally.

Cross-document transitions: the MPA case

/* On both the source and destination document. Same-origin required. */
@view-transition {
  navigation: auto;
}

That single at-rule, present on both documents involved in the navigation, is the entire opt-in. A standard <a href="/next-page"> link click now cross-fades between the old and new document instead of hard-cutting, in browsers that support it - and does an ordinary, unremarkable navigation in browsers that don’t. This is the progressive-enhancement property that makes the feature safe to ship regardless of the cross-document support ambiguity above: unsupported browsers get a normal navigation, not a broken one.

Naming specific elements to morph them individually

/* On the list page */
.product-card[data-id="42"] { view-transition-name: product-42; }

/* On the detail page for the same product */
.product-hero { view-transition-name: product-42; }

When the browser recognizes the same view-transition-name on an element present in both the outgoing and incoming document, it doesn’t cross-fade that element - it morphs position, size, and (to a degree) shape between the two states. A product thumbnail in a grid expanding into a full hero image on navigation, without a single line of animation code, is the canonical demonstration and a genuinely production-useful pattern for listing-to-detail flows.

// Dynamic names, generated per item - common in a loop rendering many cards.
document.querySelectorAll('.product-card').forEach(card => {
  card.style.viewTransitionName = `product-${card.dataset.id}`;
});

Constrain this. Every named element creates an additional snapshot layer the browser must track through the transition. Name only the elements that meaningfully benefit from individual morphing - a hero image, a title, a price - not every element on the page. An unconstrained rule naming every card in a 40-item grid is the most common way this feature is shipped badly.

The pseudo-element tree, and what it lets you customize

A view transition constructs a pseudo-element tree during its lifetime:

::view-transition
└─ ::view-transition-group(name)
   └─ ::view-transition-image-pair(name)
      ├─ ::view-transition-old(name)
      └─ ::view-transition-new(name)
/* Slow down and customize the default crossfade for the root transition */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 400ms;
}

/* A named element gets its own custom animation instead of the default */
::view-transition-old(product-42),
::view-transition-new(product-42) {
  animation: none;
}
::view-transition-group(product-42) {
  animation-duration: 500ms;
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}

Inspect this directly rather than guessing at values: Chrome DevTools’ Animations panel shows an active view transition as a distinct animation group, and the Elements panel exposes the ::view-transition pseudo-tree live during the transition, so you can adjust timing values against what’s actually rendering rather than iterating blind.

Performance discipline - this is compositor work, but it isn’t free

The default cross-fade is GPU-composited and does not, by itself, touch the main thread in an expensive way. Two things still can:

Work inside the startViewTransition callback. If your callback does an expensive synchronous DOM mutation or triggers layout, that work happens on the main thread before the transition can start, and a slow callback delays the whole interaction. Keep the callback itself cheap.

Excessive named elements. As noted above - each named element is an additional layer the browser must snapshot, size, and composite. This scales, and it stops being free at some point specific to your page’s complexity. Measure rather than assume it’s free at any count.

Respect motion preference, as with any animation covered on this site:

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Pairing with Speculation Rules

The combination that actually delivers the “feels like an SPA” result: Speculation Rules prerenders the likely next document before the click; the View Transition animates into it once navigation fires. Without prerendering, a cross-document transition still has to wait on the network for the new document, and a 400ms crossfade into a loading skeleton is a worse experience than no transition at all. The transition makes the navigation feel seamless; the prerender makes sure there’s something ready to transition into.

Frequently asked questions

Are cross-document view transitions supported everywhere?

No, and coverage of exactly where they are supported conflicts across sources as of this writing. Chromium support (Chrome 126+) is consistently reported. Safari and Firefox status is reported inconsistently across sources checked for this article - verify against caniuse or test directly before relying on a specific browser’s support.

Do I need a fallback if the browser doesn’t support this?

Not functionally. An unsupported browser performs the navigation normally, without the transition animation. The feature is progressive enhancement by construction.

What’s the difference between the default transition and a named view-transition-name?

The default is a cross-fade of the whole viewport. A shared view-transition-name present on matching elements in both documents causes the browser to morph that specific element’s position and size between states instead of fading it.

How many elements should I name?

As few as produce a meaningfully better transition - a hero image, a title. Every named element adds a snapshot layer; naming everything in a large list degrades performance for no proportional benefit.

Does this replace client-side routing?

For an MPA where every “page” is a real navigable document, yes, in combination with Speculation Rules for prefetch/prerender. It doesn’t apply to a genuine SPA with in-memory state that isn’t represented as separate documents.

Sources


Related posts