Use React's <ViewTransition> component to animate client-side navigations. This requires React and React DOM 19.3 or later.
React Router's viewTransition props/options, useViewTransitionState hook, and NavLink's isTransitioning render prop and transitioning class are deprecated in favor of React's <ViewTransition> component. The existing APIs continue to work, including with older React versions.
Wrap the outlet in a persistent <ViewTransition> boundary in a shared layout route. In Framework Mode, this can be your root route's default export; in Data and Declarative modes, use it as the component for a parent route with children:
import { ViewTransition } from "react";
import { Link, Outlet } from "react-router";
export default function PageLayout() {
return (
<>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
<ViewTransition>
<main>
<Outlet />
</main>
</ViewTransition>
</>
);
}
Navigations that update the outlet cross-fade its contents. Keep the boundary mounted across navigations; you don't need to key it by location or remount the route tree. The navigation stays outside the animated region. Other updates inside the boundary can also animate when they run in a React Transition.
React Router wraps router state updates in React.startTransition by default, which lets React activate <ViewTransition> without additional configuration. React Transitions must remain enabled for navigation view transitions to work: do not set useTransitions={false} on your router.
This applies to navigations from <Link>, <NavLink>, and <Form>, as well as programmatic navigation with useNavigate and useSubmit. No additional startTransition wrapper is needed.
viewTransition from <Link>, <NavLink>, and <Form>, and remove viewTransition: true from navigation/submission options. React coordinates document.startViewTransition() itself; enabling both implementations can interrupt animations.useViewTransitionState, isTransitioning, and .transitioning styles with React's View Transition classes. Those router APIs only track the legacy transitions.<ViewTransition name="..."> with the same unique name. See React's shared element example.useTransitions={false} and synchronous updates such as flushSync can prevent React's animations.React's component is not a drop-in replacement for every legacy animation. In particular, React skips view transitions for synchronous updates from popstate, so browser Back/Forward navigations may not animate. Browsers without View Transitions support still navigate normally.
Respect reduced-motion preferences when adding animations:
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
The following examples document the deprecated APIs for existing applications. These APIs are available in Framework and Data modes.
The simplest way to enable view transitions is by adding the viewTransition prop to your Link, NavLink, or Form components. This automatically wraps the navigation update in document.startViewTransition().
<Link to="/about" viewTransition>
About
</Link>
Without any additional CSS, this provides a basic cross-fade animation between pages.
When using programmatic navigation with the useNavigate hook, you can enable view transitions by passing the viewTransition: true option:
import { useNavigate } from "react-router";
function NavigationButton() {
const navigate = useNavigate();
return (
<button
onClick={() =>
navigate("/about", { viewTransition: true })
}
>
About
</button>
);
}
This provides the same cross-fade animation as using the viewTransition prop on Link components.
For more information on using the View Transitions API, please refer to the "Smooth transitions with the View Transition API" guide from the Google Chrome team.
Let's build an image gallery that demonstrates how to trigger and use view transitions. We'll create a list of images that expand into a detail view with smooth animations.
import { NavLink } from "react-router";
export const images = [
"https://remix.run/blog-images/headers/the-future-is-now.jpg",
"https://remix.run/blog-images/headers/waterfall.jpg",
"https://remix.run/blog-images/headers/webpack.png",
// ... more images ...
];
export default function ImageGalleryRoute() {
return (
<div className="image-list">
<h1>Image List</h1>
<div>
{images.map((src, idx) => (
<NavLink
key={src}
to={`/image/${idx}`}
viewTransition // Enable view transitions for this link
>
<p>Image Number {idx}</p>
<img
className="max-w-full contain-layout"
src={src}
/>
</NavLink>
))}
</div>
</div>
);
}
Define view transition names and animations for elements that should transition smoothly between routes.
/* Layout styles for the image grid */
.image-list > div {
display: grid;
grid-template-columns: repeat(4, 1fr);
column-gap: 10px;
}
.image-list h1 {
font-size: 2rem;
font-weight: 600;
}
.image-list img {
max-width: 100%;
contain: layout;
}
.image-list p {
width: fit-content;
}
/* Assign transition names to elements during navigation */
.image-list a.transitioning img {
view-transition-name: image-expand;
}
.image-list a.transitioning p {
view-transition-name: image-title;
}
The detail view needs to use the same view transition names to create a seamless animation.
import { Link } from "react-router";
import { images } from "./home";
import type { Route } from "./+types/image-details";
export default function ImageDetailsRoute({
params,
}: Route.ComponentProps) {
return (
<div className="image-detail">
<Link to="/" viewTransition>
Back
</Link>
<h1>Image Number {params.id}</h1>
<img src={images[Number(params.id)]} />
</div>
);
}
/* Match transition names from the list view */
.image-detail h1 {
font-size: 2rem;
font-weight: 600;
width: fit-content;
view-transition-name: image-title;
}
.image-detail img {
max-width: 100%;
contain: layout;
view-transition-name: image-expand;
}
You can control view transitions more precisely using either render props or the useViewTransitionState hook.
<NavLink to={`/image/${idx}`} viewTransition>
{({ isTransitioning }) => (
<>
<p
style={{
viewTransitionName: isTransitioning
? "image-title"
: "none",
}}
>
Image Number {idx}
</p>
<img
src={src}
style={{
viewTransitionName: isTransitioning
? "image-expand"
: "none",
}}
/>
</>
)}
</NavLink>
useViewTransitionState hookfunction NavImage(props: { src: string; idx: number }) {
const href = `/image/${props.idx}`;
// Hook provides transition state for specific route
const isTransitioning = useViewTransitionState(href);
return (
<Link to={href} viewTransition>
<p
style={{
viewTransitionName: isTransitioning
? "image-title"
: "none",
}}
>
Image Number {props.idx}
</p>
<img
src={props.src}
style={{
viewTransitionName: isTransitioning
? "image-expand"
: "none",
}}
/>
</Link>
);
}