DevDockTools

CSS View Transitions API: What Works Today, What Doesn't

A practical breakdown of what the View Transitions API can actually ship in production today, its real browser gaps, and where it still falls short.

By Daniel Agrici6 min read
view transitions apicss animationsspa navigationbrowser compatibilityweb animations

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.

Frequently Asked Questions

Does the View Transitions API work across multiple pages, not just SPAs?
Yes, as of the cross-document view transitions extension, you can get animated transitions between full page navigations (MPAs) using the @view-transition CSS at-rule, not just single-page app route changes. Support for this cross-document mode is newer and narrower than the same-document API.
Does the View Transitions API work in Firefox?
As of this writing, Firefox has not shipped the View Transitions API, same-document or cross-document. Chromium browsers (Chrome, Edge) support it, and Safari has been adding support incrementally. Always feature-detect with `document.startViewTransition` before calling it.
Can I animate individual elements differently within one transition?
Yes, by assigning unique `view-transition-name` values to elements you want to animate independently. Each named element gets its own pseudo-element pair (old and new state) that you can target in CSS separately from the default root transition.