<ViewTransition>
<ViewTransition> lets you animate a component tree with Transitions and Suspense.
import {ViewTransition} from 'react';
<ViewTransition>
<div>...</div>
</ViewTransition>- Reference
- Styling View Transitions
- Usage
- Animating an element on enter/exit
- Animating a shared element
- Animating reorder of items in a list
- Animating from Suspense content
- Opting-out of an animation
- Customizing animations
- Customizing animations with types
- Animating with JavaScript
- Animating transition types with JavaScript
- Building View Transition enabled routers
- Troubleshooting
Reference
<ViewTransition>
Wrap a component tree in <ViewTransition> to animate it:
<ViewTransition>
<Page />
</ViewTransition>Deep Dive
Under the hood, React applies view-transition-name to inline styles of the nearest DOM node nested inside the <ViewTransition> component. If there are multiple sibling DOM nodes like <ViewTransition><div /><div /></ViewTransition> then React adds a suffix to the name to make each unique but conceptually they’re part of the same one. React doesn’t apply these eagerly but only at the time that boundary should participate in an animation.
React automatically calls startViewTransition itself behind the scenes so you should never do that yourself. In fact, if you have something else on the page running a ViewTransition React will interrupt it. So it’s recommended that you use React itself to coordinate these. If you had other ways to trigger ViewTransitions in the past, we recommend that you migrate to the built-in way.
If there are other React ViewTransitions already running then React will wait for them to finish before starting the next one. However, importantly if there are multiple updates happening while the first one is running, those will all be batched into one. If you start A->B. Then in the meantime you get an update to go to C and then D. When the first A->B animation finishes the next one will animate from B->D.
The getSnapshotBeforeUpdate lifecycle will be called before startViewTransition and some view-transition-name will update at the same time.
Then React calls startViewTransition. Inside the updateCallback, React will:
- Apply its mutations to the DOM and invoke
useInsertionEffect. - Wait for fonts to load.
- Call
componentDidMount,componentDidUpdate,useLayoutEffectand refs. - Wait for any pending Navigation to finish.
- Then React will measure any changes to the layout to see which boundaries will need to animate.
After the ready Promise of the startViewTransition is resolved, React will then revert the view-transition-name. Then React will invoke the onEnter, onExit, onUpdate and onShare callbacks to allow for manual programmatic control over the animations. This will be after the built-in default ones have already been computed.
If a flushSync happens to get in the middle of this sequence, then React will skip the Transition since it relies on being able to complete synchronously.
After the finished Promise of the startViewTransition is resolved, React will then invoke useEffect. This prevents those from interfering with the performance of the animation. However, this is not a guarantee because if another setState happens while the animation is running it’ll still have to invoke the useEffect earlier to preserve the sequential guarantees.
Props
- optional
name: A string or object. The name of the View Transition used for shared element transitions. If not provided, React will use a unique name for each View Transition to prevent unexpected animations. - View Transition Class props.
- View Transition Event props.
Caveats
- Only use
namefor shared element transitions. For all other animations, React automatically generates a unique name to prevent unexpected animations. - By default,
setStateupdates immediately and does not activate<ViewTransition>, only updates wrapped in a Transition,<Suspense>, oruseDeferredValueactivate ViewTransition. <ViewTransition>creates an image that can be moved around, scaled and cross-faded. Unlike Layout Animations you may have seen in React Native or Motion, this means that not every individual Element inside of it animates its position. This can lead to better performance and a more continuous feeling, smooth animation compared to animating every individual piece. However, it can also lose continuity in things that should be moving by themselves. So you might have to add more<ViewTransition>boundaries manually as a result.- Currently,
<ViewTransition>only works in the DOM. We’re working on adding support for React Native and other platforms.
Animation triggers
React automatically decides the type of View Transition animation to trigger:
enter: If aViewTransitionis the first component inserted in this Transition, then this will activate.exit: If aViewTransitionis the first component deleted in this Transition, then this will activate.update: If aViewTransitionhas any DOM mutations inside it that React is doing (such as a prop changing) or if theViewTransitionboundary itself changes size or position due to an immediate sibling. If there are nestedViewTransitionthen the mutation applies to them and not the parent.share: If a namedViewTransitionis inside a deleted subtree and another namedViewTransitionwith the same name is part of an inserted subtree in the same Transition, they form a Shared Element Transition, and it animates from the deleted one to the inserted one.
By default, <ViewTransition> animates with a smooth cross-fade (the browser default view transition).
You can customize the animation by providing a View Transition Class to the <ViewTransition> component for each kind of trigger (see Styling View Transitions), or by using ViewTransition Events to control the animation with JavaScript using the Web Animations API.
View Transition Class
<ViewTransition> provides props to define what animations trigger:
<ViewTransition
default="none"
enter="slide-up"
exit="slide-down"
/>Props
- optional
enter:"auto","none", a string, or an object. - optional
exit:"auto","none", a string, or an object. - optional
update:"auto","none", a string, or an object. - optional
share:"auto","none", a string, or an object. - optional
default:"auto","none", a string, or an object.
Caveats
- If
defaultis"none"then all other triggers are turned off unless explicitly listed.
Values
View Transition class values can be:
auto: the default. Uses the browser default animation.none: disable animations for this type.<classname>: a custom CSS class name to use for customizing View Transitions.
Object values can be an object with string keys and a value of auto, none or a custom className:
{[type]: value}: appliesvalueif the animation matches the Transition Type.{default: value}: the default value to apply if no Transition Type is matched.
For example, you can define a ViewTransition as:
<ViewTransition
/* turn off any animation not defined below */
default="none"
enter={{
/* apply slide-in for Transition Type `forward` */
"forward": 'slide-in',
/* otherwise use the browser default animation */
"default": 'auto'
}}
/* use the browser default for exit animations*/
exit="auto"
/* apply a custom `cross-fade` class for updates */
update="cross-fade"
>See Styling View Transitions for how to define CSS classes for custom animations.
View Transition Event
View Transition Events allow you to control the animation with JavaScript using the Web Animations API:
<ViewTransition
onEnter={instance => {/* ... */}}
onExit={instance => {/* ... */}}
/>Props
- optional
onEnter: Called when an “enter” animation is triggered. - optional
onExit: Called when an “exit” animation is triggered. - optional
onShare: Called when a “share” animation is triggered. - optional
onUpdate: Called when an “update” animation is triggered.
Caveats
- Only one event fires per
<ViewTransition>per Transition.onSharetakes precedence overonEnterandonExit. - Each event should return a cleanup function. The cleanup function is called when the View Transition finishes, allowing you to cancel or cleanup any animations.
Arguments
Each event receives two arguments:
instance: A View Transition instance that provides access to the view transition pseudo-elementsold: The::view-transition-oldpseudo-element.new: The::view-transition-newpseudo-element.name: Theview-transition-namestring for this boundary.group: The::view-transition-grouppseudo-element.imagePair: The::view-transition-image-pairpseudo-element.
types: AnArray<string>of Transition Types included in the animation. Empty array if no types were specified.
For example, you can define a onEnter event that drives the animation using JavaScript:
<ViewTransition
onEnter={(instance, types) => {
const anim = instance.new.animate([{opacity: 0}, {opacity: 1}], {
duration: 500,
});
return () => anim.cancel();
}}>
<div>...</div>
</ViewTransition>See Animating with JavaScript for more examples.
Styling View Transitions
To customize the animation for a <ViewTransition> you can provide a View Transition Class to one of the activation props. The View Transition Class is a CSS class name that React applies to the child elements when the ViewTransition activates.
For example, to customize an “enter” animation, provide a class name to the enter prop:
<ViewTransition enter="slide-in">When the <ViewTransition> activates an “enter” animation, React will add the class name slide-in. Then you can refer to this class using view transition pseudo selectors to build reusable animations:
::view-transition-group(.slide-in) {
}
::view-transition-old(.slide-in) {
}
::view-transition-new(.slide-in) {
}In the future, CSS libraries may add built-in animations using View Transition Classes to make this easier to use.
Usage
Animating an element on enter/exit
Enter/Exit Transitions trigger when a <ViewTransition> is added or removed by a component in a transition:
function Child() {
return (
<ViewTransition>
<div>Hi</div>
</ViewTransition>
);
}
function Parent() {
const [show, setShow] = useState();
if (show) {
return <Child />;
}
return null;
}When setShow is called, show switches to true and the Child component is rendered. When setShow is called inside startTransition, and Child renders a ViewTransition before any other DOM nodes, an enter animation is triggered.
When show switches back to false, an exit animation is triggered.
import {ViewTransition, useState, startTransition} from 'react'; import {Video} from './Video'; import videos from './data'; function Item() { return ( <ViewTransition> <Video video={videos[0]} /> </ViewTransition> ); } export default function Component() { const [showItem, setShowItem] = useState(false); return ( <> <button onClick={() => { startTransition(() => { setShowItem((prev) => !prev); }); }}> {showItem ? '➖' : '➕'} </button> {showItem ? <Item /> : null} </> ); }
Animating a shared element
Normally, we don’t recommend assigning a name to a <ViewTransition> and instead let React assign it an automatic name. The reason you might want to assign a name is to animate between completely different components when one tree unmounts and another tree mounts at the same time, to preserve continuity.
<ViewTransition name={UNIQUE_NAME}>
<Child />
</ViewTransition>When one tree unmounts and another mounts, if there’s a pair where the same name exists in the unmounting tree and the mounting tree, they trigger the “share” animation on both. It animates from the unmounting side to the mounting side.
Unlike an exit/enter animation this can be deeply inside the deleted/mounted tree. If a <ViewTransition> would also be eligible for exit/enter, then the “share” animation takes precedence.
If Transition first unmounts one side and then leads to a <Suspense> fallback being shown before eventually the new name being mounted, then no shared element transition happens.
import {ViewTransition, useState, startTransition} from 'react'; import {Video, Thumbnail, FullscreenVideo} from './Video'; import videos from './data'; export default function Component() { const [fullscreen, setFullscreen] = useState(false); if (fullscreen) { return ( <FullscreenVideo video={videos[0]} onExit={() => startTransition(() => setFullscreen(false))} /> ); } return ( <Video video={videos[0]} onClick={() => startTransition(() => setFullscreen(true))} /> ); }
Animating reorder of items in a list
items.map((item) => <Component key={item.id} item={item} />);When reordering a list, without updating the content, the “update” animation triggers on each <ViewTransition> in the list if they’re outside a DOM node. Similar to enter/exit animations.
This means that this will trigger the animation on this <ViewTransition>:
function Component() {
return (
<ViewTransition>
<div>...</div>
</ViewTransition>
);
}import {ViewTransition, useState, startTransition} from 'react'; import {Video} from './Video'; import videos from './data'; export default function Component() { const [orderedVideos, setOrderedVideos] = useState(videos); const reorder = () => { startTransition(() => { setOrderedVideos((prev) => { return [...prev.sort(() => Math.random() - 0.5)]; }); }); }; return ( <> <button onClick={reorder}>🎲</button> <div className="listContainer"> {orderedVideos.map((video, i) => { return ( <ViewTransition key={video.title}> <Video video={video} /> </ViewTransition> ); })} </div> </> ); }
However, this wouldn’t animate each individual item:
function Component() {
return (
<div>
<ViewTransition>...</ViewTransition>
</div>
);
}Instead, any parent <ViewTransition> would cross-fade. If there is no parent <ViewTransition> then there’s no animation in that case.
import {ViewTransition, useState, startTransition} from 'react'; import {Video} from './Video'; import videos from './data'; export default function Component() { const [orderedVideos, setOrderedVideos] = useState(videos); const reorder = () => { startTransition(() => { setOrderedVideos((prev) => { return [...prev.sort(() => Math.random() - 0.5)]; }); }); }; return ( <> <button onClick={reorder}>🎲</button> <ViewTransition> <div className="listContainer"> {orderedVideos.map((video, i) => { return <Video video={video} key={video.title} />; })} </div> </ViewTransition> </> ); }
This means you might want to avoid wrapper elements in lists where you want to allow the Component to control its own reorder animation:
items.map(item => <div><Component key={item.id} item={item} /></div>)The above rule also applies if one of the items updates to resize, which then causes the siblings to resize, it’ll also animate its sibling <ViewTransition> but only if they’re immediate siblings.
This means that during an update, which causes a lot of re-layout, it doesn’t individually animate every <ViewTransition> on the page. That would lead to a lot of noisy animations which distracts from the actual change. Therefore React is more conservative about when an individual animation triggers.
Animating from Suspense content
Like any Transition, React waits for data and new CSS (<link rel="stylesheet" precedence="...">) before running the animation. In addition to this, ViewTransitions also wait up to 500ms for new fonts to load before starting the animation to avoid them flickering in later. For the same reason, an image wrapped in ViewTransition will wait for the image to load.
If it’s inside a new Suspense boundary instance, then the fallback is shown first. After the Suspense boundary fully loads, it triggers the <ViewTransition> to animate the reveal to the content.
There are two ways to animate Suspense boundaries depending on where you place the <ViewTransition>:
Update:
<ViewTransition>
<Suspense fallback={<A />}>
<B />
</Suspense>
</ViewTransition>In this scenario when the content goes from A to B, it’ll be treated as an “update” and apply that class if appropriate. Both A and B will get the same view-transition-name and therefore they’re acting as a cross-fade by default.
import {ViewTransition, useState, startTransition, Suspense} from 'react'; import {Video, VideoPlaceholder} from './Video'; import {useLazyVideoData} from './data'; function LazyVideo() { const video = useLazyVideoData(); return <Video video={video} />; } export default function Component() { const [showItem, setShowItem] = useState(false); return ( <> <button onClick={() => { startTransition(() => { setShowItem((prev) => !prev); }); }}> {showItem ? '➖' : '➕'} </button> {showItem ? ( <ViewTransition> <Suspense fallback={<VideoPlaceholder />}> <LazyVideo /> </Suspense> </ViewTransition> ) : null} </> ); }
Enter/Exit:
<Suspense fallback={<ViewTransition><A /></ViewTransition>}>
<ViewTransition><B /></ViewTransition>
</Suspense>In this scenario, these are two separate ViewTransition instances each with their own view-transition-name. This will be treated as an “exit” of the <A> and an “enter” of the <B>.
You can achieve different effects depending on where you choose to place the <ViewTransition> boundary.
Opting-out of an animation
Sometimes you’re wrapping a large existing component, like a whole page, and you want to animate some updates, such as changing the theme. However, you don’t want it to opt-in all updates inside the whole page to cross-fade when they’re updating. Especially if you’re incrementally adding more animations.
You can use the class “none” to opt-out of an animation. By wrapping your children in a “none” you can disable animations for updates to them while the parent still triggers.
<ViewTransition>
<div className={theme}>
<ViewTransition update="none">{children}</ViewTransition>
</div>
</ViewTransition>This will only animate if the theme changes and not if only the children update. The children can still opt-in again with their own <ViewTransition> but at least it’s manual again.
Customizing animations
By default, <ViewTransition> includes the default cross-fade from the browser.
To customize animations, you can provide props to the <ViewTransition> component to specify which animations to use, based on how the <ViewTransition> activates.
For example, we can slow down the default cross fade animation:
<ViewTransition default="slow-fade">
<Video />
</ViewTransition>And define slow-fade in CSS using view transition classes:
::view-transition-old(.slow-fade) {
animation-duration: 500ms;
}
::view-transition-new(.slow-fade) {
animation-duration: 500ms;
}import {ViewTransition, useState, startTransition} from 'react'; import {Video} from './Video'; import videos from './data'; function Item() { return ( <ViewTransition default="slow-fade"> <Video video={videos[0]} /> </ViewTransition> ); } export default function Component() { const [showItem, setShowItem] = useState(false); return ( <> <button onClick={() => { startTransition(() => { setShowItem((prev) => !prev); }); }}> {showItem ? '➖' : '➕'} </button> {showItem ? <Item /> : null} </> ); }
In addition to setting the default, you can also provide configurations for enter, exit, update, and share animations.
import {ViewTransition, useState, startTransition} from 'react'; import {Video} from './Video'; import videos from './data'; function Item() { return ( <ViewTransition enter="slide-in" exit="slide-out"> <Video video={videos[0]} /> </ViewTransition> ); } export default function Component() { const [showItem, setShowItem] = useState(false); return ( <> <button onClick={() => { startTransition(() => { setShowItem((prev) => !prev); }); }}> {showItem ? '➖' : '➕'} </button> {showItem ? <Item /> : null} </> ); }
Customizing animations with types
You can use the addTransitionType API to add a class name to the child elements when a specific transition type is activated for a specific activation trigger. This allows you to customize the animation for each type of transition.
For example, to customize the animation for all forward and backward navigations:
<ViewTransition
default={{
'navigation-back': 'slide-right',
'navigation-forward': 'slide-left',
}}>
<div>...</div>
</ViewTransition>;
// in your router:
startTransition(() => {
addTransitionType('navigation-' + navigationType);
});When the ViewTransition activates a “navigation-back” animation, React will add the class name “slide-right”. When the ViewTransition activates a “navigation-forward” animation, React will add the class name “slide-left”.
In the future, routers and other libraries may add support for standard view-transition types and styles.
import { ViewTransition, addTransitionType, useState, startTransition, } from 'react'; import {Video} from './Video'; import videos from './data'; function Item() { return ( <ViewTransition enter={{ 'add-video-back': 'slide-in-back', 'add-video-forward': 'slide-in-forward', }} exit={{ 'remove-video-back': 'slide-in-forward', 'remove-video-forward': 'slide-in-back', }}> <Video video={videos[0]} /> </ViewTransition> ); } export default function Component() { const [showItem, setShowItem] = useState(false); return ( <> <div className="button-container"> <button onClick={() => { startTransition(() => { if (showItem) { addTransitionType('remove-video-back'); } else { addTransitionType('add-video-back'); } setShowItem((prev) => !prev); }); }}> ⬅️ </button> <button onClick={() => { startTransition(() => { if (showItem) { addTransitionType('remove-video-forward'); } else { addTransitionType('add-video-forward'); } setShowItem((prev) => !prev); }); }}> ➡️ </button> </div> {showItem ? <Item /> : null} </> ); }
Animating with JavaScript
While View Transition Classes let you define animations with CSS, sometimes you need imperative control over the animation. The onEnter, onExit, onUpdate, and onShare callbacks give you direct access to the view transition pseudo-elements so you can animate them using the Web Animations API.
Each callback receives an instance with .old and .new properties representing the view transition pseudo-elements. You can call .animate() on them just like you would on a DOM element:
<ViewTransition
onEnter={(instance) => {
const anim = instance.new.animate(
[
{transform: 'scale(0.8)'},
{transform: 'scale(1)'},
],
{duration: 300, easing: 'ease-out'}
);
return () => anim.cancel();
}}>
<div>...</div>
</ViewTransition>This allows you to combine CSS-driven animations and JavaScript-driven animations.
In the following example, the default cross-fade is handled by CSS, and the slide animations are driven by JavaScript in the onEnter and onExit animations:
import {ViewTransition, useState, startTransition} from 'react'; import {Video} from './Video'; import videos from './data'; import {SLIDE_IN, SLIDE_OUT} from './animations'; function Item() { return ( <ViewTransition default="none" /* CSS driven cross fade defaults */ enter="auto" exit="auto" /* JS driven slide animations */ onEnter={(instance) => { const anim = instance.new.animate( SLIDE_IN, {duration: 500, easing: 'ease-out'} ); return () => anim.cancel(); }} onExit={(instance) => { const anim = instance.old.animate( SLIDE_OUT, {duration: 300, easing: 'ease-in'} ); return () => anim.cancel(); }}> <Video video={videos[0]} /> </ViewTransition> ); } export default function Component() { const [showItem, setShowItem] = useState(false); return ( <> <button onClick={() => { startTransition(() => { setShowItem((prev) => !prev); }); }}> {showItem ? '➖' : '➕'} </button> {showItem ? <Item /> : null} </> ); }
Animating transition types with JavaScript
You can use types passed to ViewTransition events to conditionally apply different animations based on how the Transition was triggered.
<ViewTransition
onEnter={(instance, types) => {
const duration = types.includes('fast') ? 150 : 2000;
const anim = instance.new.animate(
SLIDE_IN,
{duration: duration, easing: 'ease-out'}
);
return () => anim.cancel();
}}
>This example calls addTransitionType to mark a Transition as “fast” and then adjust the animation duration:
import {ViewTransition, useState, startTransition, addTransitionType} from 'react'; import {Video} from './Video'; import videos from './data'; import {SLIDE_IN, SLIDE_OUT} from './animations'; function Item() { return ( <ViewTransition onEnter={(instance, types) => { const duration = types.includes('fast') ? 150 : 2000; const anim = instance.new.animate( SLIDE_IN, {duration: duration, easing: 'ease-out'} ); return () => anim.cancel(); }} onExit={(instance, types) => { const duration = types.includes('fast') ? 150 : 500; const anim = instance.old.animate( SLIDE_OUT, {duration: duration, easing: 'ease-in'} ); return () => anim.cancel(); }}> <Video video={videos[0]} /> </ViewTransition> ); } export default function Component() { const [showItem, setShowItem] = useState(false); const [isFast, setIsFast] = useState(false); return ( <> <div> Fast: <input type="checkbox" onChange={() => {setIsFast(f => !f)}} value={isFast}></input> </div><br /> <button onClick={() => { startTransition(() => { if (isFast) { addTransitionType('fast'); } setShowItem((prev) => !prev); }); }}> {showItem ? '➖' : '➕'} </button> {showItem ? <Item /> : null} </> ); }
Building View Transition enabled routers
React waits for any pending Navigation to finish to ensure that scroll restoration happens within the animation. If the Navigation is blocked on React, your router must unblock in useLayoutEffect since useEffect would lead to a deadlock.
If a startTransition is started from the legacy popstate event, such as during a “back”-navigation then it must finish synchronously to ensure scroll and form restoration works correctly. This is in conflict with running a View Transition animation. Therefore, React will skip animations from popstate and animations won’t run for the back button. You can fix this by upgrading your router to use the Navigation API.
Troubleshooting
My <ViewTransition> is not activating
<ViewTransition> only activates if it is placed before any DOM node:
function Component() {
return (
<div>
<ViewTransition>Hi</ViewTransition>
</div>
);
}To fix, ensure that the <ViewTransition> comes before any other DOM nodes:
function Component() {
return (
<ViewTransition>
<div>Hi</div>
</ViewTransition>
);
}I’m getting an error “There are two <ViewTransition name=%s> components with the same name mounted at the same time.”
This error occurs when two <ViewTransition> components with the same name are mounted at the same time:
function Item() {
// 🚩 All items will get the same "name".
return <ViewTransition name="item">...</ViewTransition>;
}
function ItemList({items}) {
return (
<>
{items.map((item) => (
<Item key={item.id} />
))}
</>
);
}This will cause the View Transition to error. In development, React detects this issue to surface it and logs two errors:
<ViewTransition name=%s> components with the same name mounted at the same time. This is not supported and will cause View Transitions to error. Try to use a more unique name e.g. by using a namespace prefix and adding the id of an item to the name.
at Item
at ItemList<ViewTransition name=%s> duplicate has this stack trace.
at Item
at ItemListTo fix, ensure that there’s only one <ViewTransition> with the same name mounted at a time in the entire app by ensuring the name is unique, or adding an id to the name:
function Item({id}) {
// ✅ All items will get a unique name.
return <ViewTransition name={`item-${id}`}>...</ViewTransition>;
}
function ItemList({items}) {
return (
<>
{items.map((item) => (
<Item key={item.id} item={item} />
))}
</>
);
}