Skip to content

Commit 43f559c

Browse files
committed
Suspense animation example
1 parent 083c535 commit 43f559c

File tree

1 file changed

+227
-2
lines changed

1 file changed

+227
-2
lines changed

src/content/reference/react/ViewTransition.md

Lines changed: 227 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,7 +1055,7 @@ It's important to properly use keys to preserve identity when reordering lists.
10551055

10561056
### Animating from Suspense content {/*animating-from-suspense-content*/}
10571057

1058-
Just 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. In the future we plan on also waiting for images.
1058+
Just 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.
10591059

10601060
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.
10611061

@@ -1074,11 +1074,236 @@ Update:
10741074
```
10751075
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.
10761076

1077+
<Sandpack>
1078+
1079+
```js src/Video.js hidden
1080+
function Thumbnail({ video, children }) {
1081+
return (
1082+
<div
1083+
aria-hidden="true"
1084+
tabIndex={-1}
1085+
className={`thumbnail ${video.image}`}
1086+
/>
1087+
);
1088+
}
1089+
1090+
export function Video({ video }) {
1091+
return (
1092+
<div className="video">
1093+
<div className="link">
1094+
<Thumbnail video={video}></Thumbnail>
1095+
<div className="info">
1096+
<div className="video-title">{video.title}</div>
1097+
<div className="video-description">{video.description}</div>
1098+
</div>
1099+
</div>
1100+
</div>
1101+
);
1102+
}
1103+
1104+
export function VideoPlaceholder() {
1105+
const video = {image: "loading"}
1106+
return (
1107+
<div className="video">
1108+
<div className="link">
1109+
<Thumbnail video={video}></Thumbnail>
1110+
<div className="info">
1111+
<div className="video-title loading" />
1112+
<div className="video-description loading" />
1113+
</div>
1114+
</div>
1115+
</div>
1116+
);
1117+
}
1118+
```
1119+
1120+
```js
1121+
import {
1122+
unstable_ViewTransition as ViewTransition,
1123+
useState,
1124+
startTransition,
1125+
Suspense
1126+
} from 'react';
1127+
import {Video, VideoPlaceholder} from "./Video";
1128+
import {useLazyVideoData} from "./data"
1129+
1130+
function LazyVideo() {
1131+
const video = useLazyVideoData();
1132+
return (
1133+
<Video video={video}/>
1134+
);
1135+
}
1136+
1137+
export default function Component() {
1138+
const [showItem, setShowItem] = useState(false);
1139+
return (
1140+
<>
1141+
<button
1142+
onClick={() => {
1143+
startTransition(() => {
1144+
setShowItem((prev) => !prev);
1145+
});
1146+
}}
1147+
>{showItem ? '' : ''}</button>
1148+
1149+
{showItem ? (
1150+
<ViewTransition>
1151+
<Suspense fallback={<VideoPlaceholder />}>
1152+
<LazyVideo />
1153+
</Suspense>
1154+
</ViewTransition>
1155+
) : null}
1156+
</>
1157+
);
1158+
}
1159+
```
1160+
1161+
```js src/data.js hidden
1162+
import {use} from "react";
1163+
1164+
let cache = null;
1165+
1166+
function fetchVideo() {
1167+
if (!cache) {
1168+
cache = new Promise((resolve) => {
1169+
setTimeout(() => {
1170+
resolve({
1171+
id: '1',
1172+
title: 'First video',
1173+
description: 'Video description',
1174+
image: 'blue',
1175+
});
1176+
}, 1000);
1177+
});
1178+
}
1179+
return cache;
1180+
}
1181+
1182+
export function useLazyVideoData() {
1183+
return use(fetchVideo());
1184+
}
1185+
```
1186+
1187+
1188+
```css
1189+
#root {
1190+
display: flex;
1191+
flex-direction: column;
1192+
align-items: center;
1193+
min-height: 200px;
1194+
}
1195+
button {
1196+
border: none;
1197+
border-radius: 50%;
1198+
width: 50px;
1199+
height: 50px;
1200+
display: flex;
1201+
justify-content: center;
1202+
align-items: center;
1203+
background-color: #f0f8ff;
1204+
color: white;
1205+
font-size: 20px;
1206+
cursor: pointer;
1207+
transition: background-color 0.3s, border 0.3s;
1208+
}
1209+
button:hover {
1210+
border: 2px solid #ccc;
1211+
background-color: #e0e8ff;
1212+
}
1213+
.thumbnail {
1214+
position: relative;
1215+
aspect-ratio: 16 / 9;
1216+
display: flex;
1217+
overflow: hidden;
1218+
flex-direction: column;
1219+
justify-content: center;
1220+
align-items: center;
1221+
border-radius: 0.5rem;
1222+
outline-offset: 2px;
1223+
width: 8rem;
1224+
vertical-align: middle;
1225+
background-color: #ffffff;
1226+
background-size: cover;
1227+
user-select: none;
1228+
}
1229+
.thumbnail.blue {
1230+
background-image: conic-gradient(at top right, #c76a15, #087ea4, #2b3491);
1231+
}
1232+
.loading {
1233+
background-image: linear-gradient(90deg, rgba(173, 216, 230, 0.3) 25%, rgba(135, 206, 250, 0.5) 50%, rgba(173, 216, 230, 0.3) 75%);
1234+
background-size: 200% 100%;
1235+
animation: shimmer 1.5s infinite;
1236+
}
1237+
@keyframes shimmer {
1238+
0% {
1239+
background-position: -200% 0;
1240+
}
1241+
100% {
1242+
background-position: 200% 0;
1243+
}
1244+
}
1245+
.video {
1246+
display: flex;
1247+
flex-direction: row;
1248+
gap: 0.75rem;
1249+
align-items: center;
1250+
margin-top: 1em;
1251+
}
1252+
.video .link {
1253+
display: flex;
1254+
flex-direction: row;
1255+
flex: 1 1 0;
1256+
gap: 0.125rem;
1257+
outline-offset: 4px;
1258+
cursor: pointer;
1259+
}
1260+
.video .info {
1261+
display: flex;
1262+
flex-direction: column;
1263+
justify-content: center;
1264+
margin-left: 8px;
1265+
gap: 0.125rem;
1266+
}
1267+
.video .info:hover {
1268+
text-decoration: underline;
1269+
}
1270+
.video-title {
1271+
font-size: 15px;
1272+
line-height: 1.25;
1273+
font-weight: 700;
1274+
color: #23272f;
1275+
}
1276+
.video-title.loading {
1277+
height: 20px;
1278+
width: 80px;
1279+
}
1280+
.video-description {
1281+
color: #5e687e;
1282+
font-size: 13px;
1283+
}
1284+
.video-description.loading {
1285+
height: 15px;
1286+
width: 100px;
1287+
}
1288+
```
1289+
1290+
```json package.json hidden
1291+
{
1292+
"dependencies": {
1293+
"react": "experimental",
1294+
"react-dom": "experimental",
1295+
"react-scripts": "latest"
1296+
}
1297+
}
1298+
```
1299+
1300+
</Sandpack>
1301+
10771302
Enter/Exit:
10781303

10791304
```
10801305
<Suspense fallback={<ViewTransition><A /></ViewTransition>}>
1081-
<ViewTransition><B /></ViewTransition>
1306+
<ViewTransition><B /></ViewTransition>
10821307
</Suspense>
10831308
```
10841309

0 commit comments

Comments
 (0)