React View Transitions: A Hands-On Look
A practical walkthrough of the View Transitions API in React — what works, what does not work, and whether it is ready for real projects.
Smooth page transitions on the web have historically required heavyweight animation libraries or manual CSS transitions with state management. The View Transitions API provides a native solution, and React 19 has started integrating support for it. I've been experimenting with it, and here is what I've found.
What the View Transitions API Does
The API captures a "before" snapshot of the DOM, lets us make changes, then animates between the old and new states. The browser takes screenshots of elements, cross-fades them, and interpolates positions.
Basic vanilla usage:
document.startViewTransition(() => {
// Update the DOM however you want
document.querySelector('.content').innerHTML = newContent;
});The browser snapshots the old state, runs our callback, snapshots the new state, then animates between them. We don't need to write keyframes, deal with FLIP calculations, or worry about layout thrashing.
Using It in React
React 19 introduced the <ViewTransition> component and integrated startViewTransition into its rendering pipeline. React can now coordinate its state updates with the browser's transition mechanism.
A basic example with React Router:
import { ViewTransition } from 'react';
import { useNavigate } from 'react-router-dom';
function PostCard({ post }) {
const navigate = useNavigate();
return (
<ViewTransition name={`post-${post.id}`}>
<article
onClick={() => navigate(`/post/${post.id}`)}
style={{ cursor: 'pointer' }}
>
<img src={post.thumbnail} alt={post.title} />
<h3>{post.title}</h3>
</article>
</ViewTransition>
);
}
function PostDetail({ post }) {
return (
<ViewTransition name={`post-${post.id}`}>
<article>
<img src={post.image} alt={post.title} />
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
</ViewTransition>
);
}The name prop is the key — matching ViewTransition names across navigations causes one element to morph into the other. The thumbnail on the list page expands into the hero image on the detail page without any animation code.
Controlling the Animation with CSS
The API exposes pseudo-elements for styling:
/* Customize the transition duration */
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 300ms;
}
/* Make a specific element slide instead of fade */
::view-transition-old(post-hero) {
animation: slide-out 250ms ease-in;
}
::view-transition-new(post-hero) {
animation: slide-in 250ms ease-out;
}
@keyframes slide-out {
to { transform: translateX(-100%); opacity: 0; }
}
@keyframes slide-in {
from { transform: translateX(100%); opacity: 0; }
}view-transition-class can be used to group elements and animate them together with shared CSS rules.
Integration with React's Concurrent Rendering
React ties view transitions to its concurrent rendering model. State updates inside a transition are batched and wrapped in startViewTransition. This works with useTransition:
import { useTransition } from 'react';
function Tabs({ tabs }) {
const [activeTab, setActiveTab] = useState(0);
const [isPending, startTransition] = useTransition();
return (
<div>
<nav>
{tabs.map((tab, i) => (
<button
key={i}
onClick={() => {
startTransition(() => setActiveTab(i));
}}
>
{tab.label}
</button>
))}
</nav>
<ViewTransition>
<div className={isPending ? 'loading' : ''}>
{tabs[activeTab].content}
</div>
</ViewTransition>
</div>
);
}React's concurrent rendering ensures the new content is ready before the transition starts, avoiding a flash of incomplete UI.
What Works Well
Built into the browser. We don't need Framer Motion or GSAP — the browser handles it natively with GPU-level compositing.
Shared element transitions. The list-to-detail pattern — where a card morphs into a full page — works well with very little code.
Progressive enhancement. If the browser does not support the API, nothing breaks. The transition simply does not happen. Detection is straightforward:
const supportsViewTransitions = 'startViewTransition' in document;Current Limitations
Browser support. Cross-document view transitions (MPA) are available in Chrome and Edge. Firefox and Safari are still catching up. Same-document transitions have broader support.
Complex transitions. Beyond simple fades and morphs, we are back to writing detailed CSS animations. Animations where elements change size, position, and content simultaneously can produce visual artifacts.
Debugging. The pseudo-element approach makes it harder to inspect transition state in DevTools compared to normal elements.
React's integration is still evolving. The <ViewTransition> component works, but the ergonomics around conditional transitions and transition groups may change.
Route Transitions Pattern
A more complete pattern for animating between routes:
import { ViewTransition } from 'react';
import { Routes, Route, useLocation } from 'react-router-dom';
function App() {
const location = useLocation();
return (
<ViewTransition>
<Routes location={location}>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/post/:id" element={<Post />} />
</Routes>
</ViewTransition>
);
}With some CSS, every route change gets a smooth transition:
::view-transition-old(root) {
animation: fade-and-scale-out 200ms ease-in forwards;
}
::view-transition-new(root) {
animation: fade-and-scale-in 200ms ease-out forwards;
}
@keyframes fade-and-scale-out {
to {
opacity: 0;
transform: scale(0.95);
}
}
@keyframes fade-and-scale-in {
from {
opacity: 0;
transform: scale(1.05);
}
}When to Use It
Internal tools, Electron apps, PWAs where Chrome/Edge dominance is fine — the API is stable enough for same-document transitions.
Public-facing sites — treat it as progressive enhancement. Chrome users get transitions, others get a standard instant swap.
Complex animation requirements — keep the animation library for now. The View Transitions API handles common cases well but is not a replacement for something like Framer Motion when we need gesture-driven animations or spring physics.
The direction is right though. Browser-native transitions with framework integration is exactly where this should be heading. The less JavaScript we ship for visual polish, the better.