Page Transitions Are Back, But They Need Taste
View Transitions API and Motion for React make page animation easy. Easy isn't good. Here's how to use them without turning your app into a PowerPoint.
The View Transitions API is now available in all major browsers. Motion for React ships layout animations that just work. Between the two, adding page transitions to a React app has never been cheaper. That cheapness is a trap. The hardest part of page transitions was never the implementation — it was knowing when they communicate something useful and when they are decoration that slows users down.
What the View Transitions API Actually Does
The API captures a screenshot of the current DOM state, applies the new DOM state, then animates between the two captures. By default, it crossfades the entire viewport over 300ms. Same-document transitions (within a SPA) and cross-document transitions (between full page navigations) work differently.
Cross-document transitions require a @view-transition rule in your CSS and no JavaScript:
@view-transition {
navigation: auto;
}
That is the entire opt-in for multi-page apps. Chrome will crossfade between pages automatically. This is the correct starting point for content-heavy sites like blogs or marketing pages.
Same-document transitions are triggered in JavaScript:
document.startViewTransition(() => {
// Mutate the DOM here — update React state, navigate, etc.
flushSync(() => setCurrentView("detail"));
});
With React and Next.js App Router, you wrap the router call:
import { useRouter } from "next/navigation";
import { flushSync } from "react-dom";
function useViewTransitionRouter() {
const router = useRouter();
const navigate = (href: string) => {
if (!document.startViewTransition) {
router.push(href);
return;
}
document.startViewTransition(() => {
flushSync(() => router.push(href));
});
};
return { navigate };
}
Note the flushSync — without it, React batches the state update and the transition captures nothing useful. This is the most common implementation bug.
The 150–300ms Sweet Spot
Duration is where most transitions go wrong. The default 300ms crossfade is already at the edge of acceptable. Beyond 400ms, users notice the wait. Below 100ms, the transition is imperceptible and you have done work for nothing.
The principle: transition duration should match perceived navigation distance. A drill-down from a list to a detail view warrants a 200–250ms slide. Switching between tabs at the same level warrants 150ms or less. A modal appearing over content warrants 200ms with ease-out. A full-page navigation between distinct sections can go up to 300ms.
Here is a well-tuned transition for a list-to-detail pattern using view-transition-name:
/* List item that becomes the detail page header */
.product-card {
view-transition-name: product-hero;
}
.product-detail-hero {
view-transition-name: product-hero;
}
/* The shared element morphs; everything else crossfades */
::view-transition-old(product-hero),
::view-transition-new(product-hero) {
animation-duration: 250ms;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 200ms;
}
The shared element (product-hero) gets a slightly longer duration than the root crossfade because it is carrying the visual narrative — your eye should follow it across the transition.
Transitions That Communicate vs Transitions That Perform
A good transition answers a spatial question: where did I come from and where am I going?
A slide from right to left says "I went deeper." A slide from left to right says "I went back." A crossfade says "I changed context at the same level." A scale-up from a small element says "this thing I tapped became the current view."
A bad transition is one that does something cool without answering any spatial question. A flip. A cube rotate. A ripple from the center of the screen. These are all demonstrations of what the API can do, not communications about navigation. After the novelty wears off — which takes about two interactions — they feel like they are in the way.
The practical test: could you remove this transition and would the user be confused about where they are? If yes, the transition is doing navigation work. If no, the transition is decoration and probably should be removed or shortened.
Motion for React's Role
Motion for React (motion/react, formerly Framer Motion) handles a different layer than View Transitions. Where View Transitions handles the moment of navigation, Motion handles the state within a view — list items that reorder, panels that expand, cards that respond to input.
The layout prop is the relevant piece:
import { motion, AnimatePresence } from "motion/react";
function FilteredList({ items }: { items: Item[] }) {
return (
<ul>
<AnimatePresence>
{items.map((item) => (
<motion.li
key={item.id}
layout
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.15 }}
>
{item.name}
</motion.li>
))}
</AnimatePresence>
</ul>
);
}
When filters change and items reorder, layout calculates the delta between old and new positions and animates the move. This is genuinely communicative — users can track which items left and which stayed.
Do not use View Transitions and Motion's layout for the same element. They will fight. Use View Transitions for the navigation boundary, Motion for in-view state changes.
When to Skip Transitions Entirely
Three cases where you should not animate at all:
Reduced motion preference. This is not optional. If a user has enabled prefers-reduced-motion, your transitions should either disappear or collapse to an instant cut. The View Transitions API respects this in cross-document mode automatically. Same-document transitions need explicit handling:
const prefersReduced = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
if (!prefersReduced && document.startViewTransition) {
document.startViewTransition(() => flushSync(() => router.push(href)));
} else {
router.push(href);
}
Fast repeat interactions. If a user is clicking through a list rapidly — think photo galleries, autocomplete selections, paginated results — transitions are in the way. They want speed, not theater. Either skip the transition at high-frequency interaction rates or use durations under 120ms.
Data-heavy route changes. If the new route requires a network request before it can render meaningfully, a transition from the current page into a half-loaded skeleton is worse than a simple loading state. Let the data arrive, then render. A transition into emptiness communicates nothing.
What Bad Transitions Look Like
I will be direct: most page transitions I see in production apps are too long, too elaborate, and communicate nothing about navigation structure. They exist because someone saw them in a Dribbble mockup and wanted to ship "that feel."
The tell is the animation-duration: 600ms in the CSS. No navigation transition needs 600ms. At 600ms, you are making the user wait a full half-second for every navigation. Multiply that by 20 navigations in a session and you have added 12 seconds of friction in the name of polish.
Another tell: transitions that are identical regardless of direction. If going to a detail page and going back to the list both crossfade, the transition is not communicating directionality — it is just happening.
The goal is a product where users describe navigation as "fast" and "smooth" without being able to articulate why. They should not say "I love the transitions." They should not notice them at all.