May 9, 2025 First publish on Mar 17, 2023
Using View Transition API in React App
The View Transition API is a new feature on web that simplifies the process of creating animated transitions for shared element. Previously, achieving smooth transitions for shared element on the web was a complex task. However, with the introduction of this API, we can now easily animate shared element, allows us to create engaging and fluid navigational experiences, similar to those found in mobile applications.
In this article, I will provide a brief overview of how to start using View Transition API in your React applications. For a more comprehensive guide, please refer to this article authored by Jake Archibald, who championed this feature.
A simple example
In the example below, when the Move button is clicked, the cat image will switch from top right to bottom left (or vice versa) abruptly, as they are technically two separate elements.
Use the play button on the right to run the example.
document.getElementById('moveBtn').addEventListener('click', () => {
document
.querySelectorAll('.thumbnail, .cat-details')
.forEach((target) => target.classList.toggle('hidden'));
});
js
<div class="top-bar">
<div class="top-bar-content">
<h1>Move Cat</h1>
<button id="moveBtn">Move</button>
</div>
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
width="300"
height="300"
class="cat-img thumbnail"
/>
</div>
<div class="cat-details hidden">
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_500/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
width="500"
height="500"
class="cat-img detailed-img"
/>
<div class="cat-desc">
<h2>Cat Details</h2>
</div>
</div>
<style>
.cat-details {
display: flex;
}
.hidden {
display: none;
}
.cat-desc {
flex: 1;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: start;
}
.top-bar-content {
display: flex;
align-items: center;
gap: 2rem;
}
.thumbnail {
width: 100px;
height: 100px;
}
.detailed-img {
width: 50vw;
height: auto;
}
</style>
html
document.getElementById('moveBtn').addEventListener('click', () => {
document
.querySelectorAll('.thumbnail, .cat-details')
.forEach((target) => target.classList.toggle('hidden'));
});
<div class="top-bar">
<div class="top-bar-content">
<h1>Move Cat</h1>
<button id="moveBtn">Move</button>
</div>
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
width="300"
height="300"
class="cat-img thumbnail"
/>
</div>
<div class="cat-details hidden">
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_500/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
width="500"
height="500"
class="cat-img detailed-img"
/>
<div class="cat-desc">
<h2>Cat Details</h2>
</div>
</div>
<style>
.cat-details {
display: flex;
}
.hidden {
display: none;
}
.cat-desc {
flex: 1;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: start;
}
.top-bar-content {
display: flex;
align-items: center;
gap: 2rem;
}
.thumbnail {
width: 100px;
height: 100px;
}
.detailed-img {
width: 50vw;
height: auto;
}
</style>
Using View Transition API
Using View Transition API, we can animate that movement with the following changes:
- wrap the code that update the DOM element with
document.startViewTransition
. - add
view-transition-name
CSS property with the same value to the two cat images that we want to animate.
If you’re using latest version of Chrome, you’ll see the following changes when the Move button is clicked:
- The cat image will transition between two positions with smooth animation.
- There is a subtle fade animation for the “Cat Details” text.
document.getElementById('moveBtn').addEventListener('click', () => {
document.startViewTransition(() => {
document
.querySelectorAll('.thumbnail, .cat-details')
.forEach((target) => target.classList.toggle('hidden'));
});
});
js
<div class="top-bar">
<div class="top-bar-content">
<h1>Move Cat</h1>
<button id="moveBtn">Move</button>
</div>
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
width="300"
height="300"
class="cat-img thumbnail"
/>
</div>
<div class="cat-details hidden">
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_500/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
width="500"
height="500"
class="cat-img detailed-img"
/>
<div class="cat-desc">
<h2>Cat Details</h2>
</div>
</div>
<style>
.cat-img {
view-transition-name: meow-image;
}
.cat-details {
display: flex;
}
.hidden {
display: none;
}
.cat-desc {
flex: 1;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: start;
}
.top-bar-content {
display: flex;
align-items: center;
gap: 2rem;
}
.thumbnail {
width: 100px;
height: 100px;
}
.detailed-img {
width: 50vw;
height: auto;
}
</style>
html
document.getElementById('moveBtn').addEventListener('click', () => {
document.startViewTransition(() => {
document
.querySelectorAll('.thumbnail, .cat-details')
.forEach((target) => target.classList.toggle('hidden'));
});
});
<div class="top-bar">
<div class="top-bar-content">
<h1>Move Cat</h1>
<button id="moveBtn">Move</button>
</div>
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
width="300"
height="300"
class="cat-img thumbnail"
/>
</div>
<div class="cat-details hidden">
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_500/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
width="500"
height="500"
class="cat-img detailed-img"
/>
<div class="cat-desc">
<h2>Cat Details</h2>
</div>
</div>
<style>
.cat-img {
view-transition-name: meow-image;
}
.cat-details {
display: flex;
}
.hidden {
display: none;
}
.cat-desc {
flex: 1;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: start;
}
.top-bar-content {
display: flex;
align-items: center;
gap: 2rem;
}
.thumbnail {
width: 100px;
height: 100px;
}
.detailed-img {
width: 50vw;
height: auto;
}
</style>
How it works
You can understand how the API works in this way:
-
When
document.startViewTransition
is called, browser- takes a screenshot of the entire page
- finds all the elements with
view-transition-name
CSS property declared, and takes a screenshot of them
In the example above, it takes a screenshot of the entire page, and then a screenshot of the cat image.
-
Once the screenshots are done, it will invoke the callback that you passed to it. In the example above, the callback will hide the cat image on top right and display the cat image on bottom left.
-
Once the callback is done, it then take another round of screenshots like step 1, then figures out the difference and perform the animation accordingly.
- by default, the animation is fading effect, which is why the “Cat Details” has the fade animation. (To be more precise, entire page has the fade animation, but for parts that did not change, we can’t observe the fade animation)
- for elements with
view-transition-name
property defined, it will figure out the difference of the position, and animate accordingly.
If you want to understand it in a more accurate and detailed way, refer to this section of Jake Archibald’s article.
Usage View Transition API with React
Because React renders state changes asynchronously, we need to wrap the state-setter function with flushSync
to force the state changes to be applied synchronously.
import * as React from 'react';
import { flushSync } from 'react-dom';
import { createRoot } from 'react-dom/client';
const App = () => {
const [isThumbnail, setIsThumbnail] = React.useState(true);
const handleMove = () => {
document.startViewTransition(() => {
flushSync(() => {
setIsThumbnail((prev) => !prev);
});
});
};
return (
<div>
<div className="top-bar">
<div className="top-bar-content">
<h1>Move Cat</h1>
<button onClick={handleMove}>Move</button>
</div>
{isThumbnail && (
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
className="cat-img thumbnail"
/>
)}
</div>
{!isThumbnail && (
<div className="cat-details">
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_500/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
className="cat-img detailed-img"
/>
<div className="cat-desc">
<h2>Cat Details</h2>
</div>
</div>
)}
</div>
);
};
createRoot(document.getElementById('root')).render(<App />);
jsx
<div id="root"></div>
<style>
.cat-img {
view-transition-name: meow-image;
}
.cat-details {
display: flex;
}
.hidden {
display: none;
}
.cat-desc {
flex: 1;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: start;
}
.top-bar-content {
display: flex;
align-items: center;
gap: 2rem;
}
.thumbnail {
width: 100px;
height: 100px;
}
.detailed-img {
width: 50vw;
height: auto;
}
</style>
html
import * as React from 'react';
import { flushSync } from 'react-dom';
import { createRoot } from 'react-dom/client';
const App = () => {
const [isThumbnail, setIsThumbnail] = React.useState(true);
const handleMove = () => {
document.startViewTransition(() => {
flushSync(() => {
setIsThumbnail((prev) => !prev);
});
});
};
return (
<div>
<div className="top-bar">
<div className="top-bar-content">
<h1>Move Cat</h1>
<button onClick={handleMove}>Move</button>
</div>
{isThumbnail && (
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
className="cat-img thumbnail"
/>
)}
</div>
{!isThumbnail && (
<div className="cat-details">
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_500/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
className="cat-img detailed-img"
/>
<div className="cat-desc">
<h2>Cat Details</h2>
</div>
</div>
)}
</div>
);
};
createRoot(document.getElementById('root')).render(<App />);
<div id="root"></div>
<style>
.cat-img {
view-transition-name: meow-image;
}
.cat-details {
display: flex;
}
.hidden {
display: none;
}
.cat-desc {
flex: 1;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: start;
}
.top-bar-content {
display: flex;
align-items: center;
gap: 2rem;
}
.thumbnail {
width: 100px;
height: 100px;
}
.detailed-img {
width: 50vw;
height: auto;
}
</style>
Coming soon: React first-class support with ViewTransition component
React team is working on a new component, ViewTransition
that will integrates with View Transition API, as announced in this blog.
import * as React from 'react'; // @version: react@0.0.0-experimental-38ef6550-20250508
import { createRoot } from 'react-dom/client'; // @version: react-dom@0.0.0-experimental-38ef6550-20250508
const { unstable_ViewTransition: ViewTransition } = React;
const App = () => {
const [isThumbnail, setIsThumbnail] = React.useState(true);
const handleMove = () => {
React.startTransition(() => {
setIsThumbnail((prev) => !prev);
});
};
return (
<div>
<div className="top-bar">
<div className="top-bar-content">
<h1>Move Cat</h1>
<button onClick={handleMove}>Move</button>
</div>
{isThumbnail && (
<ViewTransition name="meow-image">
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
className="thumbnail"
/>
</ViewTransition>
)}
</div>
{!isThumbnail && (
<div className="cat-details">
<ViewTransition name="meow-image">
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_500/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
className="detailed-img"
/>
</ViewTransition>
<div className="cat-desc">
<h2>Cat Details</h2>
</div>
</div>
)}
</div>
);
};
createRoot(document.getElementById('root')).render(<App />);
jsx
<div id="root"></div>
<style>
.cat-details {
display: flex;
}
.hidden {
display: none;
}
.cat-desc {
flex: 1;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: start;
}
.top-bar-content {
display: flex;
align-items: center;
gap: 2rem;
}
.thumbnail {
width: 100px;
height: 100px;
}
.detailed-img {
width: 50vw;
height: auto;
}
</style>
html
import * as React from 'react'; // @version: react@0.0.0-experimental-38ef6550-20250508
import { createRoot } from 'react-dom/client'; // @version: react-dom@0.0.0-experimental-38ef6550-20250508
const { unstable_ViewTransition: ViewTransition } = React;
const App = () => {
const [isThumbnail, setIsThumbnail] = React.useState(true);
const handleMove = () => {
React.startTransition(() => {
setIsThumbnail((prev) => !prev);
});
};
return (
<div>
<div className="top-bar">
<div className="top-bar-content">
<h1>Move Cat</h1>
<button onClick={handleMove}>Move</button>
</div>
{isThumbnail && (
<ViewTransition name="meow-image">
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
className="thumbnail"
/>
</ViewTransition>
)}
</div>
{!isThumbnail && (
<div className="cat-details">
<ViewTransition name="meow-image">
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_500/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
className="detailed-img"
/>
</ViewTransition>
<div className="cat-desc">
<h2>Cat Details</h2>
</div>
</div>
)}
</div>
);
};
createRoot(document.getElementById('root')).render(<App />);
<div id="root"></div>
<style>
.cat-details {
display: flex;
}
.hidden {
display: none;
}
.cat-desc {
flex: 1;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: start;
}
.top-bar-content {
display: flex;
align-items: center;
gap: 2rem;
}
.thumbnail {
width: 100px;
height: 100px;
}
.detailed-img {
width: 50vw;
height: auto;
}
</style>
With the ViewTransition
component:
- we no longer need to write custom CSS to define the
view-transition-name
property. Instead, we can use thename
prop as a replacement. - we replace
flushSync
anddocument.startViewTransition
withReact.startTransition
. Not only it’s more concise, by allowing React to handle the view transition, the behavior integrates well with React’s Suspense. For instance, we can use it withuseDeferredValue
to create animated list.
import * as React from 'react'; // @version: react@0.0.0-experimental-38ef6550-20250508
import { createRoot } from 'react-dom/client'; // @version: react-dom@0.0.0-experimental-38ef6550-20250508
const { unstable_ViewTransition: ViewTransition } = React;
const App = () => {
const [search, setSearch] = React.useState('');
const deferredSearch = React.useDeferredValue(search);
const displayedCats = deferredSearch
? catData.filter((cat) => cat.name.toUpperCase().includes(deferredSearch.toUpperCase()))
: catData;
return (
<div>
<div className="search-container">
<input
type="text"
value={search}
onChange={(ev) => setSearch(ev.target.value)}
placeholder="Type to search"
className="search-input"
/>
</div>
{displayedCats.length === 0 && (
<ViewTransition name="error-message">
<div className="error-message">
<p>No cats found</p>
</div>
</ViewTransition>
)}
<ul className="cat-list">
{displayedCats.map((cat) => (
<li key={cat.id}>
<ViewTransition name={`cat-${cat.id}`}>
<div className="cat-item">
<img src={cat.image} alt={cat.name} className="cat-thumbnail" />
<div className="cat-item-content">
<p className="cat-name">{cat.name}</p>
</div>
</div>
</ViewTransition>
</li>
))}
</ul>
</div>
);
};
const catData = [
{
id: 'meow',
name: 'Meow',
image:
'https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/meow_dtsn8h.png',
},
{
id: 'angie',
name: 'Angie',
image:
'https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/angry_cat.png',
},
{
id: 'milky',
name: 'Milky',
image:
'https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/milky.png',
},
{
id: 'spinner',
name: 'Spinner',
image:
'https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/spinning_cat.png',
},
];
createRoot(document.getElementById('root')).render(<App />);
jsx
<link rel="stylesheet" href="https://unpkg.com/modern-normalize/modern-normalize.css" />
<div id="root"></div>
<style>
.search-container {
padding: 0.25rem 0.5rem;
}
.search-input {
display: block;
width: 100%;
padding: 4px 8px;
border: 1px solid #cecece;
border-radius: 0.25rem;
}
.cat-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
list-style: none;
padding: 0;
margin: 0;
}
.cat-item {
display: flex;
gap: 1rem;
padding: 0.25rem 0.5rem;
}
.cat-thumbnail {
width: 100px;
height: 100px;
object-fit: contain;
border-radius: 0.25rem;
border: 1px solid #ccc;
}
.cat-item-content {
flex: 1;
}
.cat-name {
font-size: 1.5rem;
font-weight: 500;
}
.error-message {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
</style>
html
import * as React from 'react'; // @version: react@0.0.0-experimental-38ef6550-20250508
import { createRoot } from 'react-dom/client'; // @version: react-dom@0.0.0-experimental-38ef6550-20250508
const { unstable_ViewTransition: ViewTransition } = React;
const App = () => {
const [search, setSearch] = React.useState('');
const deferredSearch = React.useDeferredValue(search);
const displayedCats = deferredSearch
? catData.filter((cat) => cat.name.toUpperCase().includes(deferredSearch.toUpperCase()))
: catData;
return (
<div>
<div className="search-container">
<input
type="text"
value={search}
onChange={(ev) => setSearch(ev.target.value)}
placeholder="Type to search"
className="search-input"
/>
</div>
{displayedCats.length === 0 && (
<ViewTransition name="error-message">
<div className="error-message">
<p>No cats found</p>
</div>
</ViewTransition>
)}
<ul className="cat-list">
{displayedCats.map((cat) => (
<li key={cat.id}>
<ViewTransition name={`cat-${cat.id}`}>
<div className="cat-item">
<img src={cat.image} alt={cat.name} className="cat-thumbnail" />
<div className="cat-item-content">
<p className="cat-name">{cat.name}</p>
</div>
</div>
</ViewTransition>
</li>
))}
</ul>
</div>
);
};
const catData = [
{
id: 'meow',
name: 'Meow',
image:
'https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/meow_dtsn8h.png',
},
{
id: 'angie',
name: 'Angie',
image:
'https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/angry_cat.png',
},
{
id: 'milky',
name: 'Milky',
image:
'https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/milky.png',
},
{
id: 'spinner',
name: 'Spinner',
image:
'https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/spinning_cat.png',
},
];
createRoot(document.getElementById('root')).render(<App />);
<link rel="stylesheet" href="https://unpkg.com/modern-normalize/modern-normalize.css" />
<div id="root"></div>
<style>
.search-container {
padding: 0.25rem 0.5rem;
}
.search-input {
display: block;
width: 100%;
padding: 4px 8px;
border: 1px solid #cecece;
border-radius: 0.25rem;
}
.cat-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
list-style: none;
padding: 0;
margin: 0;
}
.cat-item {
display: flex;
gap: 1rem;
padding: 0.25rem 0.5rem;
}
.cat-thumbnail {
width: 100px;
height: 100px;
object-fit: contain;
border-radius: 0.25rem;
border: 1px solid #ccc;
}
.cat-item-content {
flex: 1;
}
.cat-name {
font-size: 1.5rem;
font-weight: 500;
}
.error-message {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
</style>
Using View Transition API with React Router
Since React Router version 6.27.0, using View Transition API requires only adding viewTransition
prop to Link
component. Note that the View Transition API support of React Router is not based on the React’s ViewTransition
component, therefore you can use it today as long as your React Router version is 6.27.0 or above.
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import { useNavigate, createBrowserRouter, Link, RouterProvider } from 'react-router-dom';
const TopBar = ({ link, rightContent }) => (
<div className="top-bar">
<div className="top-bar-content">
<h1>Move Cat</h1>
{link}
</div>
{rightContent}
</div>
);
const router = createBrowserRouter([
{
index: true,
element: (
<div>
<TopBar
link={
<Link to="/details" viewTransition>
Details
</Link>
}
rightContent={
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
className="cat-img thumbnail"
/>
}
/>
</div>
),
},
{
path: '/details',
element: (
<div>
<TopBar
link={
<Link to="/" viewTransition>
Home
</Link>
}
/>
<div className="cat-details">
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_500/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
className="cat-img detailed-img"
/>
<div className="cat-desc">
<h2>Cat Details</h2>
</div>
</div>
</div>
),
},
]);
createRoot(document.getElementById('root')).render(<RouterProvider router={router} />);
jsx
<div id="root"></div>
<style>
.cat-img {
view-transition-name: meow-image;
}
.cat-details {
display: flex;
}
.hidden {
display: none;
}
.cat-desc {
flex: 1;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: start;
}
.top-bar-content {
display: flex;
align-items: center;
gap: 2rem;
}
.thumbnail {
width: 100px;
height: 100px;
}
.detailed-img {
width: 50vw;
height: auto;
}
</style>
html
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import { useNavigate, createBrowserRouter, Link, RouterProvider } from 'react-router-dom';
const TopBar = ({ link, rightContent }) => (
<div className="top-bar">
<div className="top-bar-content">
<h1>Move Cat</h1>
{link}
</div>
{rightContent}
</div>
);
const router = createBrowserRouter([
{
index: true,
element: (
<div>
<TopBar
link={
<Link to="/details" viewTransition>
Details
</Link>
}
rightContent={
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_300/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
className="cat-img thumbnail"
/>
}
/>
</div>
),
},
{
path: '/details',
element: (
<div>
<TopBar
link={
<Link to="/" viewTransition>
Home
</Link>
}
/>
<div className="cat-details">
<img
src="https://res.cloudinary.com/djzsjzasg/image/upload/c_scale,w_500/v1678947391/malcolm-kee/meow_dtsn8h.png"
alt="cat"
className="cat-img detailed-img"
/>
<div className="cat-desc">
<h2>Cat Details</h2>
</div>
</div>
</div>
),
},
]);
createRoot(document.getElementById('root')).render(<RouterProvider router={router} />);
<div id="root"></div>
<style>
.cat-img {
view-transition-name: meow-image;
}
.cat-details {
display: flex;
}
.hidden {
display: none;
}
.cat-desc {
flex: 1;
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: start;
}
.top-bar-content {
display: flex;
align-items: center;
gap: 2rem;
}
.thumbnail {
width: 100px;
height: 100px;
}
.detailed-img {
width: 50vw;
height: auto;
}
</style>
Practical Use Cases
Other than transitioning between pages, some practical use cases of the View Transition API are:
-
Animating an element when open modal: When a user opens/closes a modal, transition the button to the entire modal to produce an expansion effect.
-
Animating focus ring for onboarding overlay: When navigating between steps, the focus ring and the popup modal can be moved with animation.
-
Tab active indicator animation: When switching tab, animating the active indicator used to require manual computation by considering the position of the previous active tab and the new active tab. With View Transition API we can delegates to browser to perform the computation.
Some content here
Using Progressive Enhancement Technique with View Transition
To ensure that your application still functions properly on browsers that do not support the View Transition API, you can use the progressive enhancement technique. This involves checking whether document.startViewTransition
is available before using it. If it’s not available, then no animation will be applied.
// create a wrapper to be used everywhere in your application
const transitionViewIfSupported = (updateCb) => {
if (document.startViewTransition) {
document.startViewTransition(updateCb);
} else {
updateCb();
}
};
Conclusion
In conclusion, the View Transition API offers a straightforward way to create smooth animations for web applications. By implementing it in your React projects and using progressive enhancement, you can cover a wider range of browsers while enhancing user experience. Give it a try and see the difference it makes!
Acknowledgements
Thanks Jake Archibald for his works on designing and championing this API, and also being extremely patient for all my questions about the API.