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.
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.