CSS View Transitions API: 3 Production Patterns (And 2 to Skip)

2026-04-19 · Nico Brandt

You saw that view transition demo — smooth crossfade, elements gliding between pages, navigation feeling like a native app. You added the CSS to your site, opened it on your phone, and it kind of worked. On your MacBook it was buttery. On a mid-range Android, something felt off.

The CSS view transitions API hit Baseline Newly Available in late 2025 when Firefox 144 shipped support. Every tutorial since then shows you the API. None tells you which parts to actually ship — and which ones cost more in performance and debugging than they’ll ever deliver.

Three CSS view transition patterns are production-ready: the default cross-document fade (three lines of CSS), same-document state transitions using match-element, and direction-aware page slides via the Navigation API. Browser support covers roughly 89% of global traffic. Two patterns — hero image morphs and multi-element choreography — cost more than they return.

The 70ms Tax You Didn’t Budget For

View transitions aren’t free. Most tutorials skip this part.

Real user data from 500K+ pageviews shows cross-document view transitions add approximately 70ms to LCP on mobile repeat pageviews. That’s RUM data — not a Lighthouse run on localhost. Desktop impact is negligible, around 5ms. But mobile is where your LCP budget is tightest, and on budget phones with slower CPUs, the gap widens to 77ms.

Here’s the twist: combining view transitions with Speculation Rules eliminates that LCP cost entirely. Prerendered pages absorb the transition overhead because the destination is already loaded when the animation fires. If your performance checklist doesn’t include speculation rules yet, this is your reason to add them.

The takeaway isn’t “avoid page transitions.” It’s that the pattern you choose determines whether you’re spending 0ms or 70ms of your LCP budget. Some patterns earn that cost. Some burn it.

Let’s start with the ones that earn it.

3 Patterns Worth Shipping

Three view transition patterns consistently survive the gap between demo and production. Each degrades gracefully, earns its performance cost, and solves a real navigation problem.

The Three-Line Upgrade: Cross-Document Fade

The highest-value pattern in the entire CSS view transitions API:

@view-transition {
  navigation: auto;
}

Add it to both pages. Every same-origin navigation gets a smooth crossfade instead of a hard cut. Done.

The fallback story is what makes this production-ready: browsers that don’t support cross-document view transitions just navigate normally. No polyfill. No feature detection. No JavaScript. Your site works exactly as before — roughly 89% of visitors just get a smoother experience.

Two things ship alongside it. First, the reduced-motion guard:

@media (prefers-reduced-motion: reduce) {
  @view-transition {
    navigation: none;
  }
}

Non-negotiable in production. Second, pair it with Speculation Rules to zero out that 70ms LCP cost. The prerendered destination absorbs the transition overhead completely.

This is the pattern you add on Monday and forget about. But page-level fades only smooth navigation. What about state changes within a page?

State Transitions with match-element

Tab switches, accordion expansions, list reorders, card flips — anywhere your DOM state changes, same-document view transitions make it feel intentional instead of jarring.

The core pattern:

document.startViewTransition(() => {
  updateDOM(); // your state change here
});

Wrap the DOM mutation in startViewTransition() and the browser crossfades between old and new states. The key is view-transition-name — assign matching names to elements before and after the mutation, and the browser animates between their positions.

Chrome 137+ shipped view-transition-name: match-element, which auto-names elements by DOM identity. No more manually assigning unique names to every list item or tab panel:

.card {
  view-transition-name: match-element;
}

For browsers without match-element, manual naming works everywhere the API is supported. React’s <ViewTransition> component in react@canary wraps this same mechanism — framework sugar on top, same SPA view transitions underneath.

This is the workhorse. Framework-agnostic, works with vanilla JS or React, handles the state transitions you build every week. But navigation between pages still feels flat — forward and back look identical. That’s fixable.

Direction-Aware Page Slides

Forward navigation slides content left. Back slides right. It matches the spatial mental model users already carry, and it’s the page transition pattern that makes your app feel native without framework overhead.

The pattern combines view transition types with the Navigation API for direction detection:

navigation.addEventListener('navigate', (event) => {
  const direction = getDirection(event);
  document.documentElement.dataset.transition = direction;
});
[data-transition="forward"]::view-transition-old(root) {
  animation: slide-out-left 0.25s ease;
}
[data-transition="forward"]::view-transition-new(root) {
  animation: slide-in-right 0.25s ease;
}

View transition types apply different CSS animations based on navigation direction. The Navigation API provides the directional signal.

One caveat: Navigation API support is narrower than view transitions themselves. Build the fallback — if direction detection isn’t available, fall back to the default crossfade from Pattern 1. Users still get a transition. It just won’t be directional.

Three patterns, three levels of investment. Each earns its keep. But not everything in the API does.

2 Patterns to Skip

These look stunning in conference talks. They break in the wild.

Hero Image Morphs Between Pages

Click a thumbnail, watch it expand and fly into position as the hero on the destination page. Gorgeous in a controlled demo. Fragile everywhere else.

View transition snapshots are raster images, not live DOM. CSS properties like clip-path, border-radius, and font-size don’t animate during the transition — they interpolate between two screenshots. When source and destination have different aspect ratios (and they will), the morph becomes a distorted stretch that looks worse than no transition at all.

Add the 70ms+ LCP cost on mobile without Speculation Rules, and you’re paying a performance tax for a visual artifact that makes your site feel cheaper. On slower devices, the distortion lingers long enough for users to notice — and not in the way you intended.

The fix isn’t better code. It’s accepting that hero morphs only work when you control every variable: aspect ratios, image dimensions, viewport sizes. Production doesn’t give you that control.

Multi-Element Choreography

Three or more named elements animating independently — header sliding down, sidebar fading in, content cards staggering into place. The “look what I built” pattern.

The problem isn’t primarily view transition animation performance (though cost scales with named elements). It’s debugging. Snapshot animations don’t behave like live DOM. When three overlapping snapshots interact across viewport sizes, you’re debugging combinatorial complexity with DevTools showing pseudo-elements you can’t directly manipulate.

Result: zero measurable difference to users over a simple crossfade. Maximum maintenance cost. The demo impresses other developers. Users either don’t notice — or notice when it breaks.

Both patterns optimize for the developer’s MacBook, not the user’s phone. For interaction responsiveness on budget phones, the INP guide makes the identical MacBook-vs-phone argument with real RUM data — and provides the debugging workflow for measuring what users actually experience on mid-range mobile hardware.

The Preflight Check

Before shipping any view transition, run these:

Browser support. Same-document transitions cover ~89% of global traffic — Chrome 111+, Edge 111+, Firefox 144+, Safari 18+. Cross-document transitions are narrower, with Firefox still pending. The good news: @view-transition is pure progressive enhancement. No polyfill needed. Unsupported browsers navigate normally.

Reduced-motion guard. Wrap transition CSS in @media (prefers-reduced-motion: no-preference). Not optional — a production requirement. Ship without it and you’re creating an accessibility barrier for users with vestibular disorders.

LCP budget. If mobile LCP is already near the 2.5s threshold, add Speculation Rules before adding cross-document transitions. Or stick to same-document transitions, which don’t touch navigation timing. Not sure where you stand? Run a performance audit — takes five minutes.

The one-line test. Add @view-transition { navigation: auto; } to your existing site right now. If the default crossfade feels right, you might be done. Most sites are.

Ship the Fade, Earn the Rest

You came here wondering which CSS view transitions API patterns survive real traffic on real devices. The line is clearer than most tutorials make it look.

The default crossfade ships today — three lines of CSS, zero JavaScript, graceful degradation built in. Same-document state transitions with match-element ship when your SPA needs them. Direction-aware slides ship if your navigation model supports the Navigation API. Hero morphs and choreography stay in CodePen where they belong.

Here’s what nobody says out loud: most sites only need Pattern 1. The three-line crossfade. It won’t get you conference talk invitations. But it makes every navigation feel better at zero maintenance cost — and if that’s not worth shipping, nothing is.

The best view transition is the one users don’t notice. Navigation just feels smoother. Pages feel connected instead of bolted together. The app doesn’t jolt between states.

Start with the crossfade. Measure your LCP. If you’ve got budget, add match-element for state changes. That covers 90% of what view transitions are for.