View Transitions in production: the practical bits
For two decades, smooth page-to-page motion was the exclusive property of single-page apps. Multi-page sites — which is to say, most of the web worth reading — got a white flash and a hard cut. The View Transitions API ends that asymmetry, and as of 2026 it has matured from conference demo into something we ship: this site uses it, every recent client build uses it, and the support story is finally good enough to stop calling it progressive enhancement and start calling it the enhancement.
What follows is the production guide — what the API actually does, where we use it, the fallbacks that keep it honest, and the restraint that keeps it from becoming the parallax scrolling of this decade.
What the API actually does (in one breath)
A view transition asks the browser to snapshot the old state of the page, snapshot the new state, and interpolate between them as pseudo-elements you can style with CSS. The browser freezes rendering, captures, swaps the DOM, captures again, and animates. You get crossfades for free; you get shared-element morphs — a thumbnail growing into a hero — by tagging matching elements with the same view-transition-name.
Two flavours matter:
- Same-document (
document.startViewTransition) — you call it around a DOM mutation. This is the SPAs' long-standing tool: filter a list, open a modal, resize a grid, all with continuity. - Cross-document (
@view-transition { navigation: auto }) — opted in via CSS, no JavaScript required, applied to ordinary MPA navigation between same-origin pages. This is the quiet revolution: your server-rendered site gets app-like transitions without becoming an app.
Same-document: three patterns that earn their keep
1. Filtering and sorting with continuity. When a work grid or article index re-renders after a filter change, a wrapped mutation lets surviving cards hold position while others exit. The user sees what the filter did instead of re-orienting from scratch:
@media (prefers-reduced-motion: no-preference) {
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 200ms;
}
}
function applyFilter(next: Filter) {
const update = () => setFilter(next)
if (reducedMotion() || !document.startViewTransition) return update()
document.startViewTransition(update)
}
Note the order of guards: reduced motion first, feature detection second, and the plain update runs in both fallbacks. The transition is a wrapper around state, never a branch of logic.
2. Shared-element handoffs. A card image tagged view-transition-name: card-42-image on the index, the same name on its detail page hero, and the browser morphs one into the other. This is the single highest-comprehension-value transition that exists — the user watches where the thing they clicked went. Two production rules: names must be unique per snapshot (derive them from IDs, and strip the name off the source element in the same frame you add it to the target), and you must handle the back navigation too, or users feel the asymmetry without knowing why.
3. Expand/collapse on stateful widgets. Accordions, cart drawers, inline editors. A 200ms size fade between snapshots chews zero layout code and reads as polish. Give the pseudo-elements no name and enjoy the default crossfade; over-naming is where the pain lives.
Cross-document: the MPA renaissance
For a server-rendered site, the entire setup is:
@view-transition {
navigation: auto;
}
…in a stylesheet served on both pages of any navigation you want to animate. Chrome and Edge have shipped this since 2024; Safari joined in the 18.x line; Firefox ships behind a flag with stable intent. The unsupported 30-odd percent of browsers simply get normal navigation — which is the whole beauty: the fallback is the web's old default, not a broken experience.
Three practical notes from shipping it:
Scope it. Navigation to a login flow or from a marketing page into a dashboard probably should not crossfade — different products, different moods. Use a types or route-scoped stylesheet to keep transitions within one "space."
Mind the snapshot cost on huge pages. The browser is capturing render snapshots; a 40,000-element DOM makes captures slow on mid-range phones. Our Core Web Vitals field guide instincts apply: keep long pages virtualised or paginated, and test on the thin-end devices your analytics say people actually use.
Scroll restore is yours now. During a transition the old page is a picture; scroll anchoring and position restore still work, but any scroll-linked effects (sticky headers, progress bars) freeze mid-gesture. We strip sticky offsets from ::view-transition-old snapshots so the frozen frame doesn't show a detached header floating over content.
The reduced-motion contract, kept
Everything above is wrapped in one non-negotiable: prefers-reduced-motion: no-preference gates every transition, and for cross-document navigation that's as simple as nesting the @view-transition rule inside the media query. Users who ask for stillness get instant, honest page loads. This isn't an accessibility garnish; for vestibular-sensitive users a full-page morph can be genuinely sick-making, and "we forgot the media query" is not an acceptable postmortem. The philosophy behind it — motion must answer to function — is the same one laid out in our piece on motion that earns its keep: every animation needs a verb. Morph locates. Crossfade softens. Fade-through-colour resets context. If you can't name the verb, cut it.
Where transitions genuinely improve comprehension
After a year of shipping, the patterns that survive contact with users:
- Index → detail → index (case studies, products, articles). The shared-element handoff carries identity across navigation. Measured qualitatively: in hallway tests, users stop "losing their place" in grids. This pattern is now in our default case study page kit.
- Small state changes with big consequences — filtering, theme toggle, cart updates. Continuity replaces the user's dead-reckoning about what changed.
- Onboarding steps where the progress is the point — a step counter morphing forward reinforces "you're moving through a finite thing." We leaned on it heavily in the Brightmarsh onboarding work.
And the catalogue of where we've cut them: crossfade between unrelated marketing pages (decoration; the pages aren't continuations of each other), transitions over 300ms (you are showing off with the user's time), morphing text-heavy regions (readable mid-morph fluid typography is a fantasy — snapshot text looks smeared; fade it instead), and anything near a form mid-input. Losing a keystroke to a transition is a war crime in miniature. Our form architecture rules apply: input state is sacred state.
The debugging kit
DevTools' Animations panel captures view transitions if you enable the flag while recording — indispensable for inspecting which elements got named and who blinked. Beyond that, three habits: log transition.ready rejections (they tell you when the DOM mutation failed and the animation was skipped, which is invisible otherwise); test with CPU throttling, because a transition that drops frames on a slow phone reads worse than a clean cut; and keep a kill switch — one CSS class on <html> that disables all transitions — reachable from a query param, because the first bug report that mentions it will be vague.
Key takeaways
- View Transitions give MPAs the continuity SPAs had a monopoly on:
@view-transition { navigation: auto }, no JavaScript required, unsupported browsers get plain navigation. - The highest-value pattern is the shared-element handoff between index and detail; keep names unique per snapshot and handle back navigation too.
- Gate everything behind
prefers-reduced-motion: no-preference; the API's fallback is the honest default, not a degraded one. - Cap transitions around 200–300ms, never transition live form input, and morph images and containers — fade text instead.
- Snapshot cost scales with DOM size; virtualise long pages and test on mid-range phones.
- Keep one global kill switch; your future bug reports will be too vague to debug without it.
FAQ
Is the View Transitions API ready for production in 2026? Yes, with the fallback understood. Same-document transitions are stable across Chromium and Safari; cross-document MPA transitions cover roughly 70% of global traffic and degrade to a normal page load elsewhere — no polyfill, no broken experience. If your analytics skew Firefox-desktop-heavy, treat cross-document transitions as a bonus layer and invest in the same-document patterns, which every modern framework can drive manually.
Does it work with React and other frameworks? Same-document transitions wrap any DOM mutation, so document.startViewTransition(() => flushSync(update)) works in React 18, and React 19-era tooling increasingly has first-class bindings. The discipline is unchanged: the transition wraps a state update, and nothing inside the update may know or care that it's being filmed.
Will view transitions hurt my Core Web Vitals? Not the lab metrics — transitions don't add bytes by themselves and don't count as layout shift. The real risks are interaction latency during capture on heavy pages and temptation-driven DOM bloat elsewhere. Budget the transition cost like any animation: 16ms per frame worth of work, measured on the devices your users own.
How do we adopt it incrementally on an existing site? Start with the zero-JavaScript win: add the media-query-gated @view-transition rule to a section of the site (the journal, a product listing area) and watch. Then name one shared element — index card image to detail hero — and measure the difference in user testing before going further. Incremental adoption is not just safer; it keeps each transition defensible on its own merits.
View transitions in the wild: lovely, fragile, worth it
Eight projects in, we still love the View Transitions API — with our eyes open. Where it shines, how it fights your router, and the tiers that keep it honest.
Animation is engineering: shipping motion at 60fps
Motion at 60fps is an engineering discipline: compositor-only properties, FLIP for layout changes, when canvas or WebGL earn their weight, and testing.
Theme persistence without the flash
Dark mode that flashes light on load is a broken promise. Boot scripts vs cookies, SSR the right class, reconciling system preference, and testing regressions.