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.

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:
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-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:
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.
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:
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:
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:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
- 💼 LinkedIn — linkedin.com/in/parsa-jiravand
- ✉️ Email (work & contract inquiries): bestpractice2026@gmail.com
Originally published on dev.to