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;
}
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:
- Capture β the browser snapshots the current page.
- Change β your navigation (or DOM update) runs.
-
Animate β the browser builds two pseudo-elements,
::view-transition-oldand::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); } }
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; }
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());
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();
}
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.
United States
NORTH AMERICA
Related News

Mattress Firm Coupons: Save up to $600
4h ago
π I Built a Dropshipping Automation Pipeline β Here's What I Learned (and What I'd Do Differently)
11h ago
How I Cut My LLM API Bill by 40x: A Freelancer's Migration Story
11h ago
Cursor AI Review 2026: The AI-Native Code Editor
8h ago

Another Model Rewrote My Memories. Here's How I Caught It.
8h ago