Cross-fade page transitions used to require either a JavaScript animation library or a lot of manual FLIP-technique bookkeeping. The View Transitions API replaced that with a couple of lines of code and native browser-driven interpolation — but the gap between what conference talks demo and what you can reliably ship still catches teams off guard.
What It Actually Does
document.startViewTransition(callback) takes a snapshot of the DOM before your callback runs, lets your callback mutate the DOM (swap route content, toggle visibility, whatever), takes a second snapshot after, and then cross-fades between them automatically:
function navigate(newContent) {
if (!document.startViewTransition) {
// Fallback: no animation, just swap immediately
updateDOM(newContent);
return;
}
document.startViewTransition(() => {
updateDOM(newContent);
});
}
By default you get a simple cross-fade of the entire viewport. That default alone is often enough for a route change to feel dramatically less jarring than an instant swap.
Naming Elements for Independent Transitions
The more useful case is animating specific elements — a product thumbnail growing into a hero image on a detail page, for example:
.product-thumb {
view-transition-name: product-hero;
}
.product-detail-image {
view-transition-name: product-hero;
}
When both elements share a view-transition-name, the browser treats them as the "same" element across the transition and morphs position/size between them automatically. You control the timing and easing through the generated pseudo-elements:
::view-transition-old(product-hero),
::view-transition-new(product-hero) {
animation-duration: 0.4s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
Every named element becomes its own capture, which means you can have dozens of independently-animated regions on one page transition — but each one is a rasterized snapshot, not a live DOM node, so anything with ongoing behavior (video playback, an active input focus ring) doesn't carry over cleanly.
What It Can't Do Yet
No Firefox Support
This is the biggest practical gap. Firefox has not implemented the View Transitions API as of now. Feature detection isn't optional — if (!document.startViewTransition) needs a real fallback path, not just a no-op, or Firefox users get a broken navigation rather than a plain one. Check current status on caniuse.com.
Cross-Document Transitions Are Newer and Narrower
The original API only worked for same-document (SPA-style) transitions. Cross-document view transitions — animating between two separate full page loads in a traditional MPA — arrived later via the @view-transition CSS at-rule and has a smaller support footprint than the same-document version. If your app is server-rendered with full page navigations rather than client-side routing, verify cross-document support specifically rather than assuming the same-document API numbers apply.
Snapshots Are Static Images
Because each captured state is a bitmap, not a live element, things that rely on continuous rendering — CSS animations mid-flight, video elements, canvas content, iframes — get frozen into a still frame for the transition and then resume after. For most UI chrome this is invisible, but it means you can't use view transitions to smoothly hand off an in-progress animation between two differently-styled elements; you'll get a snapshot-to-snapshot morph, not a continuation.
No Fine-Grained Control Over Which DOM Changes Trigger What
The API captures whatever changed inside your callback as a whole. You don't get automatic per-element diffing beyond what view-transition-name explicitly declares — anything without a name gets lumped into the default root transition. For complex layouts with many moving pieces, that means manually naming every element you want independent control over, which becomes real markup and CSS overhead as the count grows.
Nested/Interrupted Transitions Are Fragile
Triggering a second startViewTransition while one is still running is a known rough edge — you generally need to skip or explicitly finish the in-flight transition before starting a new one, or fast user interactions (double-clicking a nav link, rapid route changes) can produce visual glitches or unhandled promise rejections.
Respecting prefers-reduced-motion
The API doesn't automatically skip transitions for users who've set prefers-reduced-motion: reduce at the OS level — that's your responsibility, the same as with any other CSS or JS animation. The straightforward approach is to shrink the generated pseudo-element animations to effectively nothing rather than trying to intercept the JS call itself:
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
This keeps the DOM-swap behavior identical (so your startViewTransition callback logic doesn't need a separate branch) while removing the motion itself, which is exactly what the media query is asking for. Skipping this is an easy accessibility miss precisely because the API "just works" out of the box — it's a couple of lines of CSS to add during launch, not an afterthought to bolt on later. See MDN's prefers-reduced-motion docs for the full media query behavior.
Framework and Router Support
If you're using a client-side router (React Router, or a meta-framework's router) rather than calling document.startViewTransition by hand, check whether your router has built-in support before wiring it up yourself — some routing libraries expose a flag or hook that wraps route changes in a view transition automatically, handling the "wait for new content to render before taking the second snapshot" timing correctly. Wiring startViewTransition around an async route change that resolves after the callback returns is a common source of bugs: the API expects the DOM mutation to complete (including any pending layout from newly rendered content) before it captures the "after" state, so an unawaited async update can get missed entirely and produce no visible transition.
Decision Point: When to Reach for It vs. a JS Animation Library
| Situation | Recommendation | |---|---| | Chromium-first internal tool or admin panel | Use View Transitions directly, skip the fallback complexity | | Public-facing site with meaningful Firefox traffic | Use it with a real no-animation fallback, or use a JS library for consistent cross-browser behavior | | Need continuous/interactive animation (drag, scroll-linked) | Use Framer Motion, GSAP, or the Web Animations API directly — snapshots can't do this | | Simple route cross-fade, browser gaps acceptable | View Transitions API, this is exactly its sweet spot | | Complex multi-element choreography with fine timing control | JS animation library gives more control than named-element snapshots |
A Practical Middle Ground
Because the fallback for unsupported browsers is simply "no animation, instant swap," the API is close to a free enhancement: supporting browsers get a polished transition, everyone else gets exactly what they'd get without the API at all. That asymmetry is why it's reasonable to ship it now even with Firefox unsupported — you're not degrading anyone's experience below the pre-API baseline, you're only upgrading it for a subset of users.
If your transitions involve resizing elements at different viewport widths — like that thumbnail-to-hero morph — pair the transition timing with fluid sizing from the Clamp Calculator so the visual scale change reads smoothly rather than jumping at breakpoints. Start with the default root cross-fade before reaching for named transitions; it covers a surprising number of cases with zero extra CSS.