← All essays
Front-end7 min read

The View Transitions API, Practically

Cross-document transitions are here. How to add them to a plain multi-page site — and what to ship for every browser that is not there yet.

Abstract cover artwork for this essay

Single-page apps bought us fluid navigation at the cost of a routing layer, a hydration step, and a lot of JavaScript. The View Transitions API gives most of that feeling back to ordinary links between ordinary HTML files.

This journal is a multi-page site. There is no router, no virtual DOM, and no client-side rendering. Yet moving between the index and an essay feels continuous. Two small pieces do the work.

One line of CSS

Opting in to same-origin, cross-document transitions is a single at-rule in the stylesheet shared by every page:

@view-transition { navigation: auto; }

::view-transition-old(root) {
  animation: 260ms cubic-bezier(.22,1,.36,1) both fade-out;
}
::view-transition-new(root) {
  animation: 420ms cubic-bezier(.22,1,.36,1) both fade-in;
}

The browser snapshots the old page and the new one, then crossfades them. Because the navigation is still a real document load, history, deep links, prefetching, and no-JavaScript behaviour all keep working unchanged.

The fallback that matters

Not every browser supports it, and that is fine. The fallback here is a lightweight exit animation: internal links are intercepted, a class is added to the document, and navigation is deferred by a couple of hundred milliseconds. On arrival, an entrance animation runs from the top of the new document.

document.addEventListener('click', (event) => {
  const link = event.target.closest('a[href$=".html"]');
  if (!link || event.metaKey || event.ctrlKey || link.target === '_blank') return;
  if (link.origin !== location.origin) return;
  if (CSS.supports('view-transition-name: none') && 'startViewTransition' in document) return;
  event.preventDefault();
  document.body.classList.add('is-leaving');
  setTimeout(() => { location.href = link.href; }, 240);
});

The guard clauses matter more than the animation. Modified clicks, new tabs, downloads, and external links must always behave exactly as the platform intended. An animation is never a reason to take control away from the user.

Giving elements continuity

The most convincing effect is a shared element: a card on the index becomes the hero of the essay. Assigning a matching view-transition-name to both lets the browser morph one into the other. It is worth doing for one hero element and nothing else — over-assigning names creates a dozen competing animations that feel chaotic.

Respect the user

Every transition on this site is wrapped in a prefers-reduced-motion check. When a visitor asks for less motion, the wipe and reveal animations are removed and navigation becomes instant. Motion should be an enhancement, never a toll booth.

The pleasant surprise is how little code this takes. Roughly forty lines of CSS and one small event listener replaced an entire client-side routing stack — and the site got faster, not slower.