← blog
CSSJuly 3, 2026 · 5 min read

The page transition you pay a library for is now free

You reach for Framer Motion or GSAP to make navigation feel smooth. The View Transitions API does the same thing natively — and the starting price is a single CSS rule.

Parsa Jiravand · Frontend engineer · building bestpractic
The page transition you pay a library for is now free

Picture the last time you wired up a page transition. The leaving page's exit. The entering page's entrance. The awkward in-between where both exist at once. Maybe a getBoundingClientRect() here, a hand-tuned transform there, a library install to coordinate it all.

That whole apparatus was for an effect a native app gets by existing.

Here's the part that'll annoy you: the browser does it now. The simplest version — a smooth cross-fade across your entire site — costs exactly one CSS rule. The fancy version, where a thumbnail morphs into a full hero image as you navigate, costs two. And neither one needs a single line of JavaScript. Let me show you the one-rule version first, then build up to the morph.

If you've got a regular multi-page app — separate HTML documents per page — opt into automatic transitions with this:

CSS
1
2
3
@view-transition { navigation: auto; }

Drop it in your stylesheet. Navigate. Every page change is now a gentle cross-fade. No JS, no library, no event listeners, no coordination.

What's the browser actually doing? It screenshots the outgoing page, runs the navigation, then animates from that screenshot to the new page — a 300ms opacity cross-fade by default. Exactly the baseline you'd have built by hand. And it's strictly opt-in: without that rule, navigation stays instant, same as always.

That's the freebie. But "everything cross-fades" isn't why this API matters. Hold that thought.

Every view transition is the same three beats:

  • Capture — the browser snapshots the current page.
  • Change — your navigation (or DOM update) runs.
  • Animate — the browser builds two pseudo-elements, ::view-transition-old and ::view-transition-new, and tweens between them.

This is the opposite of how a motion library thinks. A library makes you describe the motion: this exits left, that enters right, here's the easing. View Transitions make you describe what should transition — the browser figures out how by diffing before against after.

Want something other than a cross-fade? Style those pseudo-elements like anything else:

CSS
1
2
3
4
5
6
7
8
::view-transition-old(root) { animation: 250ms ease-in both fade-out, 250ms ease-in both slide-out; } ::view-transition-new(root) { animation: 250ms ease-out both fade-in, 250ms ease-out both slide-in; } @keyframes slide-out { to { transform: translateX(-30px); } } @keyframes slide-in { from { transform: translateX(30px); } }

Now every navigation is a subtle directional slide. Still zero JavaScript.

Here's the thought you parked earlier. The real payoff isn't the cross-fade — it's telling the browser "this element on page A is the same element as that one on page B." Give them a shared name and the browser morphs one into the other.

CSS
1
2
3
4
5
/* list page */ .product-card[data-id="42"] img { view-transition-name: product-hero-42; } /* detail page */ .product-hero-image { view-transition-name: product-hero-42; }

Navigate from the list to the detail page and the thumbnail flies into place as the full hero — position, size, shape, all interpolated. You wrote no animation. You declared an identity, and the browser handled every pixel of geometry.

Be honest: how much code did this take last time? getBoundingClientRect() on both elements, the delta math, a transform to fake the start position, timing to undo it. A few hundred lines, easy. Here it's two rules whose names match.

One gotcha: view-transition-name must be unique in the DOM at any instant. Rendering a list of cards? Set the name dynamically so only the card being navigated carries it, or scope it with :has() to the active state.

If you own navigation programmatically, the JS API exposes that same capture-change-animate cycle:

JavaScript
1
2
3
4
5
// before: updatePageContent(); // after: document.startViewTransition(() => updatePageContent());

Pass a callback that mutates the DOM. The browser captures before, runs your function, captures after, plays the transition. Your update logic doesn't change — you wrap it. React Router and the Next.js App Router are threading this into their navigation primitives, but underneath, the primitive never changes: snapshot, update, animate.

This is the part that should make you comfortable shipping it today: it's purely additive. Browsers without support — and the holdouts are thinning, with Chrome, Firefox, and Safari all shipping it — just navigate instantly, like they always did. The guard is a one-liner:

JavaScript
1
2
3
4
5
if (document.startViewTransition) { document.startViewTransition(() => updatePageContent()); } else { updatePageContent(); }

No polyfill, no fallback CSS, no broken state. The browser plays the transition or it doesn't.

Reach for View Transitions on navigational context changes: moving between pages, opening a detail from a list, expanding an inline panel. "You're going from here to there" maps onto the API naturally.

Don't reach for it on micro-interactions inside a view: hovers, spinners, a toggle's state change, a choreographed timeline. Those want CSS transition/animation or a real motion library where you control every keyframe.

The line: animation driven by navigation between views → View Transitions. Animation driven by interaction within a view → regular CSS or a library.

The honest scope: View Transitions won't replace intricate choreography — that still needs more. What they replace is the default case. The clean cross-fade. The hero morph. The transition every multi-page app should have and usually skips, because nobody wanted to pull in Framer Motion for something that basic.

Takeaway for tomorrow: before you add an animation library for navigation, ask what one CSS rule already buys you. Often it's most of what you needed — and the rest is two matching names.

What's the page transition you abandoned because wiring it up wasn't worth it? Try the one-rule version on it tonight and tell me how close it got in the comments.


Thanks for reading! Let's stay connected:

Originally published on dev.to