DEV Community

Cover image for The page transition you pay a library for is now free
Parsa Jiravand
Parsa Jiravand

Posted on

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.

One rule. The whole site cross-fades.

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

@view-transition {
  navigation: auto;
}
Enter fullscreen mode Exit fullscreen mode

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.

The mental model that makes the rest click

Every view transition is the same three beats:

  1. Capture — the browser snapshots the current page.
  2. Change — your navigation (or DOM update) runs.
  3. 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:

::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);  } }
Enter fullscreen mode Exit fullscreen mode

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

The trick worth the price of admission

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.

/* 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; }
Enter fullscreen mode Exit fullscreen mode

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.

SPAs get the same engine, behind a function

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

// before:
updatePageContent();

// after:
document.startViewTransition(() => updatePageContent());
Enter fullscreen mode Exit fullscreen mode

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.

Adding it can't break anything

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:

if (document.startViewTransition) {
  document.startViewTransition(() => updatePageContent());
} else {
  updatePageContent();
}
Enter fullscreen mode Exit fullscreen mode

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

Where it fits — and where 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.

Back to that library you were about to install

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.

Top comments (1)

Collapse
 
frank_signorini profile image
Frank

How does this new approach handle edge cases like interrupted transitions or dynamically loaded content? I'd love to swap ideas on this, following for more insights.