Modern web applications are expected to feel responsive, not just functional. A page that changes instantly can work perfectly, but a well-designed transition can make navigation, expanding content, and changing components feel much more natural.
React has traditionally left most UI animation work to CSS animations, CSS transitions, or third-party animation libraries. React 19.3 changes the picture with the stable <ViewTransition> component, which integrates React's rendering model with the browser View Transition API.
The important part is that React does not simply provide another animation wrapper. It connects View Transitions with React Transitions, Suspense, deferred updates, and component lifecycle behavior.
This article explains how React 19.3 View Transitions work, how to implement them, how to customize animations, and what to consider before using them in a production application.
What Are View Transitions in React?
A View Transition allows the browser to visually animate a change between two UI states.
For example, consider a product listing:
A user clicks a product.
The application renders the product details.
Instead of replacing the old UI immediately, the browser creates a visual transition between the previous and next states.
React 19.3 provides the <ViewTransition> component for this purpose.
A basic example looks like this:
import { ViewTransition } from "react";
function ProductCard({ product }) {
return (
<ViewTransition>
<div className="product-card">
<h2>{product.name}</h2>
<p>{product.description}</p>
</div>
</ViewTransition>
);
}
React coordinates the browser's View Transition API behind the scenes. You generally do not need to call document.startViewTransition() yourself when using React's built-in implementation.
React 19.3 makes View Transitions stable after they were previously available as an experimental API.
How React View Transitions Work
The key concept is that <ViewTransition> describes what part of the UI should participate in an animation, while a React Transition determines when the update should be animated.
Consider this example:
import { ViewTransition, startTransition, useState } from "react";
export default function App() {
const [showDetails, setShowDetails] = useState(false);
function toggleDetails() {
startTransition(() => {
setShowDetails(value => !value);
});
}
return (
<div>
<button onClick={toggleDetails}>
{showDetails ? "Hide details" : "Show details"}
</button>
{showDetails && (
<ViewTransition>
<section className="details">
<h2>Product Details</h2>
<p>This section is displayed using a View Transition.</p>
</section>
</ViewTransition>
)}
</div>
);
}
The important detail is startTransition().
A normal state update is considered urgent:
setShowDetails(true);
A state update wrapped in startTransition() is treated as a non-urgent Transition:
startTransition(() => {
setShowDetails(true);
});
React can then use that Transition to activate the corresponding <ViewTransition>.
React documents three common transition situations:
Enter — a View Transition is added.
Exit — a View Transition is removed.
Update — content, style, position, or size changes.
Share — a named View Transition moves from one part of the UI to another.
Building an Enter and Exit Animation
A practical use case is displaying and hiding a panel.
import { ViewTransition, startTransition, useState } from "react";
export default function SettingsPanel() {
const [visible, setVisible] = useState(false);
const togglePanel = () => {
startTransition(() => {
setVisible(current => !current);
});
};
return (
<div>
<button onClick={togglePanel}>
{visible ? "Close Settings" : "Open Settings"}
</button>
{visible && (
<ViewTransition
enter="panel-enter"
exit="panel-exit"
default="none"
>
<div className="settings-panel">
<h2>Settings</h2>
<p>Manage your application preferences.</p>
</div>
</ViewTransition>
)}
</div>
);
}
Here, enter and exit specify CSS class names that React can use for the respective View Transition types.
The default="none" setting is useful when you want to explicitly control which transition types are animated instead of allowing the default animation to apply everywhere.
Customizing View Transition Animations with CSS
The default View Transition behavior is a cross-fade. For a production application, you will often want an animation that matches the application's design.
For example:
::view-transition-new(.panel-enter) {
animation: slide-in 250ms ease-out;
}
::view-transition-old(.panel-exit) {
animation: slide-out 200ms ease-in;
}
@keyframes slide-in {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slide-out {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-8px);
}
}
The React component remains focused on application behavior:
<ViewTransition
enter="panel-enter"
exit="panel-exit"
default="none"
>
<div className="settings-panel">
...
</div>
</ViewTransition>
This separation is useful because JavaScript controls the state transition while CSS controls the visual presentation.
React's documentation recommends View Transition Classes for customizing animations rather than relying on manually assigned view-transition-name values for most cases.
Animating Page Navigation
One of the most useful applications is navigation.
Imagine an application with two pages:
function App({ page }) {
return (
<ViewTransition key={page}>
{page === "home" ? <HomePage /> : <ProfilePage />}
</ViewTransition>
);
}
The navigation itself should be performed as a Transition:
import { startTransition } from "react";
function navigateToProfile() {
startTransition(() => {
setPage("profile");
});
}
This approach lets React coordinate the DOM changes with the View Transition instead of treating the animation as a separate operation.
For applications using a router, the router can integrate navigation with React Transitions, allowing page-level View Transitions to be layered onto the navigation flow. React specifically describes navigation as one of the intended use cases for View Transitions.
Shared Element Transitions
A more advanced use case is moving the same visual element between two UI states.
For example, a product image might appear as a small thumbnail on a product list and become a large image on the product details page.
You can give the corresponding View Transitions the same name:
<ViewTransition name="product-image">
<img
src={product.image}
alt={product.name}
/>
</ViewTransition>
When the old named transition is removed and another transition with the same name is inserted during the same React Transition, React can treat them as a shared element transition.
This is useful for:
Product image expansion
Profile avatar transitions
Card-to-details interfaces
Gallery navigation
Media previews
However, names must be unique while the transition is active. Accidentally mounting two View Transitions with the same name can cause problems. React recommends using a globally unique naming strategy when explicit names are required.
Using View Transitions with Suspense
React 19.3 also integrates View Transitions with Suspense.
For example:
<ViewTransition update="auto" default="none">
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails />
</Suspense>
</ViewTransition>
This can create a smooth transition when Suspense changes from its fallback content to the loaded component.
However, animation should not automatically be applied to every loading state.
For example, a skeleton that appears after a user clicks a button should normally appear immediately. Delaying it with an unnecessary animation can make the interface feel slower.
A better pattern is to animate the update from the fallback to the final content while keeping the initial fallback responsive.
<ViewTransition update="auto" default="none">
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails />
</Suspense>
</ViewTransition>
React's documentation specifically recommends being selective with Suspense animations so already-loaded content does not feel unnecessarily delayed.
Choosing Between CSS Animations and View Transitions
View Transitions do not replace traditional CSS animations.
The two approaches solve different problems.
Requirement | CSS Animation/Transition | React View Transition |
|---|---|---|
Animate a button hover | Excellent | Usually unnecessary |
Animate a loading spinner | Excellent | Not appropriate |
Animate component insertion | Possible | Excellent |
Animate page navigation | More manual | Excellent |
Animate an element between UI states | More complex | Excellent |
Animate a simple property | Excellent | Often unnecessary |
Coordinate old and new UI states | Limited | Excellent |
React Suspense integration | Manual | Built in |
Shared element transition | Complex | Supported |
A useful rule is simple: use CSS for local visual effects and View Transitions for changes between UI states.
Using Transition Types for Different Animations
Sometimes the same state update can happen for different reasons.
For example, a carousel can move forward or backward.
React provides addTransitionType() for this situation:
import {
addTransitionType,
startTransition
} from "react";
function nextSlide() {
startTransition(() => {
addTransitionType("next");
setCurrentSlide(slide => slide + 1);
});
}
function previousSlide() {
startTransition(() => {
addTransitionType("previous");
setCurrentSlide(slide => slide - 1);
});
}
You can then map those transition types to different View Transition classes:
<ViewTransition
enter={{
next: "slide-from-right",
previous: "slide-from-left"
}}
exit={{
next: "slide-to-left",
previous: "slide-to-right"
}}
>
<Slide />
</ViewTransition>
This is useful when the direction or reason for a state change should influence the animation.
Respecting Reduced Motion Preferences
Animation should not be forced on every user.
Some users configure their operating system to reduce motion because animations can cause discomfort or make interfaces harder to use.
Use the prefers-reduced-motion media query:
@media (prefers-reduced-motion: reduce) {
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
You can also replace an animation with a much shorter or less visually intensive effect.
React does not automatically disable View Transitions based on this preference, so the application should account for it explicitly.
Common Mistakes to Avoid
Using ViewTransition Without a Transition
This is one of the first issues developers encounter.
Simply rendering:
setOpen(true);
does not necessarily activate the View Transition.
For state updates that should participate in a View Transition, use a React Transition:
startTransition(() => {
setOpen(true);
});
React also activates View Transitions for relevant Suspense and deferred-value updates.
Animating Everything
Adding <ViewTransition> around every component can make an application feel busy.
Use it for meaningful UI changes:
Navigation
Opening and closing major content
Reordering content
Shared elements
Important state changes
Do not use it simply because an animation is technically possible.
Using Duplicate Names
This is particularly important for shared element transitions.
Avoid:
<ViewTransition name="item">
<Card />
</ViewTransition>
<ViewTransition name="item">
<AnotherCard />
</ViewTransition>
when both instances can be mounted at the same time.
Instead, derive names from a stable identifier when appropriate:
<ViewTransition name={`product-${product.id}`}>
<ProductCard product={product} />
</ViewTransition>
Ignoring Accessibility
A visually impressive transition is not automatically a better user experience.
Always consider reduced-motion preferences and test the interface with animations disabled.
Replacing All Existing Animation Code
View Transitions are not a replacement for every CSS or JavaScript animation.
A button press, progress indicator, skeleton shimmer, or small hover effect may still be better implemented with normal CSS.
Production Best Practices
Before introducing View Transitions into a production React application, consider the following:
Use transitions for meaningful state changes. Do not animate every render.
Keep local animations in CSS. View Transitions are most useful when the UI state itself changes.
Use
startTransition()for non-urgent updates that should participate in the transition.Keep shared transition names unique.
Test navigation and interrupted transitions. Users may click multiple controls quickly.
Respect
prefers-reduced-motion.Avoid unnecessary animation around cached content.
Test with real application layouts, especially when elements change position or size.
Use explicit transition classes when the default cross-fade is not appropriate.
Keep animations short enough that they do not make normal interaction feel slower.
Advantages and Disadvantages
Advantages
Native integration with React rendering.
Cleaner coordination between UI state and animations.
Useful for page and component transitions.
Supports enter, exit, update, and shared transitions.
Integrates with Suspense.
Can use CSS or the Web Animations API.
Reduces the amount of manual DOM transition coordination.
Disadvantages
Not appropriate for every type of animation.
Requires understanding React Transitions.
Shared transitions require careful naming.
Complex layouts can require additional View Transition boundaries.
Accessibility still needs to be handled by the application.
Support is currently focused on React DOM rather than React Native.
Troubleshooting React View Transitions
The animation does not run
Check whether the state update is happening inside startTransition().
startTransition(() => {
setPage("profile");
});
Also verify that the <ViewTransition> is actually participating in the resulting render.
The animation runs when you do not expect it
Check whether the View Transition is being activated by an update, Suspense reveal, or another transition-related update.
You can restrict behavior using:
<ViewTransition
default="none"
update="auto"
>
<Component />
</ViewTransition>
Shared elements are not transitioning
Check that:
Both elements use the same
name.The names are unique across the active application.
The old and new elements participate in the same React Transition.
The relevant elements are actually part of the viewport when required.
React treats shared transitions differently from ordinary enter and exit transitions, so layout and rendering conditions matter.
Conclusion
React 19.3 makes View Transitions a practical tool for building smoother React interfaces without requiring every animation to be managed manually.
The biggest benefit is not simply that React can animate a component. The real advantage is that React understands the relationship between the UI update and the animation. Transitions, Suspense, component insertion, removal, updates, and shared elements can all participate in the same rendering model.
For production applications, the best approach is to use View Transitions selectively. Use them for navigation, meaningful component changes, shared elements, and other UI state transitions. Keep small visual effects in CSS, respect reduced-motion preferences, and avoid adding animation where it makes an interaction feel slower.
When used with that distinction in mind, <ViewTransition> can make a React application feel significantly more connected without turning the codebase into a collection of custom animation logic.

Join the conversation! Your thoughts help the community grow.