Vivian Voss

The Page Transition You Never Had to Build

css html web

Stack Patterns ■ Episode 11

"We need a single-page application because users expect smooth transitions between pages."

One has heard this argument rather a lot over the past decade. It justified React. It justified Vue Router. It justified Framer Motion (32 KB minified). It justified Barba.js. It justified entire application architectures built around the premise that clicking a link should not, under any circumstances, feel like clicking a link.

400 KB of JavaScript so that a heading could fade whilst the URL changed. Marvellous.

The browser does it now. In CSS. Three lines.

The Pattern

Both pages include this CSS:

@view-transition {
  navigation: auto;
}

That is the entire opt-in. Click a link that navigates to another page on the same origin. The browser takes a screenshot of the old page, loads the new page, takes a screenshot of the new page, and cross-fades between them. No JavaScript. No framework. No hydration. No virtual DOM diffing the entire document tree so that a title can slide thirty pixels to the left.

The default transition is a cross-fade. It works immediately. For many sites, this is sufficient, and one does find sufficiency rather underrated.

Named Transitions

Want a specific element to animate from its old position to its new one? One CSS property:

h1 { view-transition-name: heading; }

The same view-transition-name on the corresponding element on both pages. The browser snapshots the old position, snapshots the new position, and animates between them. The element does not need to be the same DOM node. It does not need to be the same component. It does not need to exist in the same JavaScript context. It merely needs the same name.

This is the key insight: the browser matches elements across pages by name, not by identity. Two entirely separate HTML documents, served by any backend in any language, connected only by a CSS property. The elegance is rather difficult to overstate.

h1       { view-transition-name: heading; }
.hero    { view-transition-name: hero; }
nav      { view-transition-name: navigation; }
.sidebar { view-transition-name: sidebar; }

Each named element transitions independently. Unnamed elements participate in the default cross-fade.

Custom Animations

The API exposes CSS pseudo-elements for both the old and new states:

::view-transition-old(heading) {
  animation: 0.3s ease-out slide-out;
}
::view-transition-new(heading) {
  animation: 0.3s ease-in slide-in;
}

The old state and the new state are separate snapshots rendered as replaced elements. You animate them independently. The browser composites them. The result is buttery smooth because it happens in the compositor thread, not in JavaScript. You can slide, scale, rotate, clip, or apply any CSS animation you would normally use. The full power of CSS keyframes is available. No library API to learn. No React hooks to chain. Just CSS.

The Cost We Paid

Let us be honest about what the industry traded for smooth page transitions.

We abandoned server-rendered HTML. We shipped 300 KB JavaScript runtimes to the client. We broke the back button and spent engineering hours rebuilding it in JavaScript. We reinvented routing in userland because the browser's native navigation was "not smooth enough." We lost native browser caching. We invented hydration to fix the problem we created by removing HTML in the first place. We built entire state management libraries (Redux, Zustand, Jotai, Pinia, the list grows quarterly) so the client could remember what the server already knew.

We broke accessibility. Screen readers that worked perfectly with server-rendered pages now had to contend with JavaScript-mutated DOMs, focus management nightmares, and route changes that announced nothing. We broke SEO. Google had to build a headless Chrome renderer just to index SPA content. We broke the browser's loading indicator. Users no longer knew whether a page was loading or frozen. So we built skeleton screens to simulate the loading indicator we had removed.

All so a heading could slide.

What You Ship for Page Transitions Barba.js7.5 KB+ JS required Framer Motion32 KB+ React required SPA router300+ KB View Transitions0 KBCSS only. No JS. Progressive enhancement. Zero kilobytes. Because it is the browser. The runtime you already shipped.

Browser Support

Here is where honesty matters.

Full cross-document support: Chrome 126+, Edge 126+, Safari 18.2+ (including iOS Safari), Opera 112+, Samsung Internet 29+.

Partial: Firefox 146+ supports same-document view transitions, but cross-document transitions remain behind a flag as of April 2026.

Global coverage: 87.82 per cent (CanIUse, April 2026). High, but not universal. Firefox's absence from the cross-document specification is notable.

Cross-Document View Transitions: Browser Support Chrome 126+full Edge 126+full Safari 18.2+full Firefox 146+partial 87.8%global Unsupported browsers see a normal page load. No error. No fallback code. No polyfill. The feature costs nothing when absent and delights when present. Compare: SPA JavaScript fails to load = blank white page. View Transition fails = normal page load. One does note the asymmetry.

And here is why the gap does not matter as much as you think: for browsers that do not support the API, nothing breaks. The user sees a normal page load. No error. No fallback code. No polyfill. No broken layout. The feature costs nothing when absent and delights when present. That is progressive enhancement in its purest form.

Compare this with the SPA approach: if your JavaScript bundle fails to load (network timeout, CDN outage, ad blocker, corporate proxy), your users see a blank white page. The "enhanced" experience degrades to nothing. The View Transition API degrades to a normal page load. One does note the asymmetry.

The Constraints

Same-origin only. Cross-document view transitions work for same-origin navigations only. This is a security constraint: the browser needs access to both documents to snapshot them.

Unique names per page. Each view-transition-name must be unique within a single document. Two elements on the same page cannot share a name.

Performance. The browser captures raster screenshots of named elements. Naming three to five key elements (header, hero, navigation) produces smooth results. Naming everything is rather missing the point of selective enhancement.

The Point

For a decade, "smooth page transitions" was the argument that justified client-side routing, framework adoption, and the entire SPA architecture for websites that were, at their core, pages linked together with anchor tags. The browser lacked the capability, so we built it in JavaScript. Reasonably enough.

The browser has the capability now. Three lines of CSS. Zero JavaScript. Progressive enhancement built in. 87.82 per cent global support and climbing. Firefox is the last holdout for cross-document, and the same-document variant already works there.

The argument no longer applies. The question is no longer "should we use an SPA for transitions?" The question is: "what were the other reasons?" One does suspect the list is rather shorter than expected.

One does wonder how many SPAs will be reconsidered. One does rather suspect the answer is: not enough.

We shipped 400KB of JavaScript so a heading could fade. The browser does it now. Three lines of CSS. Zero JavaScript. Barba.js: 7.5KB. Framer Motion: 32KB. View Transition API: 0KB. Because it is the browser. 87.82% global coverage. Unsupported browsers see a normal page load. Progressive enhancement, done properly.