vercel-react-view-transitions
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact View Transitions
React 视图过渡(View Transitions)
React's View Transition API lets you animate between UI states using the browser's native under the hood. Declare what to animate with , trigger when with / / , and control how with CSS classes or the Web Animations API. Unsupported browsers skip the animation and apply the DOM change instantly.
document.startViewTransition<ViewTransition>startTransitionuseDeferredValueSuspenseReact 的 View Transition API 底层基于浏览器原生的 ,可帮助你实现 UI 状态之间的过渡动画。通过 声明要对什么元素做动画,通过 / / 控制什么时候触发动画,通过 CSS 类或 Web Animations API 控制动画的表现形式。不支持该 API 的浏览器会跳过动画,直接应用 DOM 变更。
document.startViewTransition<ViewTransition>startTransitionuseDeferredValueSuspenseWhen to Animate (and When Not To)
何时应该使用动画(以及何时不该用)
Every should answer: what spatial relationship or continuity does this animation communicate to the user? If you can't articulate it, don't add it.
<ViewTransition>每一个 都应该能回答这个问题:这个动画能向用户传达什么样的空间关联或上下文连续性? 如果你说不清楚,就不要加这个动画。
<ViewTransition>Hierarchy of Animation Intent
动画意图优先级
From highest value to lowest — start from the top and only move down if your app doesn't already have animations at that level:
| Priority | Pattern | What it communicates | Example |
|---|---|---|---|
| 1 | Shared element ( | "This is the same thing — I'm going deeper" | List thumbnail morphs into detail hero |
| 2 | Suspense reveal | "Data loaded, here's the real content" | Skeleton cross-fades into loaded page |
| 3 | List identity (per-item | "Same items, new arrangement" | Cards reorder during sort/filter |
| 4 | State change ( | "Something appeared or disappeared" | Panel slides in on toggle |
| 5 | Route change (layout-level) | "Going to a new place" | Cross-fade between pages |
Route-level transitions (#5) are the lowest priority because the URL change already signals a context switch. A blanket cross-fade on every navigation says nothing — it's visual noise. Prefer specific, intentional animations (#1–#4) over ambient page transitions.
Rule of thumb: at any given moment, only one level of the tree should be visually transitioning. If your pages already manage their own Suspense reveals or shared element morphs, adding a layout-level route transition on top produces double-animation where both levels fight for attention.
优先级从高到低排列,优先实现高层级的动画,只有当你的应用还没有对应层级的动画时再向下扩展:
| 优先级 | 模式 | 传达的含义 | 示例 |
|---|---|---|---|
| 1 | 共享元素( | "这是同一个对象,我正在进入更深层级的页面" | 列表缩略图变形为详情页的头图 |
| 2 | Suspense 内容露出 | "数据加载完成,这是实际内容" | 骨架屏渐变为加载完成的页面 |
| 3 | 列表元素标识(每个元素的 | "元素不变,只是排列顺序变了" | 排序/筛选时卡片重排动画 |
| 4 | 状态变更( | "有元素出现或消失了" | 切换时侧边面板滑入 |
| 5 | 路由切换(布局层级) | "正在跳转到新页面" | 页面间的淡入淡出 |
路由层级的过渡(第5类)优先级最低,因为 URL 变化本身就已经传达了上下文切换的信息。给所有导航都加统一的淡入淡出没有任何实际意义,只是视觉噪音。优先使用有明确意图的特定动画(第1-4类),而不是通用的页面过渡效果。
经验法则: 任意时刻,组件树中只能有一个层级在执行视觉过渡。如果你的页面已经自己实现了 Suspense 内容露出或共享元素变形动画,再在布局层级加路由过渡就会导致双重动画,两个层级的效果会互相干扰。
Choosing the Right Animation Style
选择合适的动画风格
Not everything should slide. Match the animation to the spatial relationship:
| Context | Animation | Why |
|---|---|---|
| Detail page main content | | Reveals "deeper" content the user drilled into |
| Detail page outer wrapper | | Navigating forward — horizontal direction |
| List / overview pages | Bare | Lateral navigation — no spatial depth to communicate |
| Page headers / breadcrumbs | Bare | Small, fast-loading metadata — slide feels excessive |
| Secondary section on same page | | Second Suspense boundary streaming in after the header |
| Revalidation / background refresh | | Data refreshed silently — animation would be distracting |
When in doubt, use a bare (default cross-fade) or . Only add directional motion (slide-up, slide-from-right) when it communicates spatial meaning.
<ViewTransition>default="none"不是所有动画都应该用滑动效果,动画类型要匹配空间关联关系:
| 场景 | 动画 | 原因 |
|---|---|---|
| 详情页主体内容 | | 展示用户点进的「更深层级」内容 |
| 详情页外层容器 | 给 | 向前导航 —— 水平方向的过渡 |
| 列表/概览页面 | 纯 | 横向导航 —— 不需要传达空间深度 |
| 页面头部/面包屑 | 纯 | 体积小、加载快的元数据 —— 滑动效果过于夸张 |
| 同一页面的次级模块 | | 头部加载完成后流式渲染的第二个 Suspense 边界 |
| 数据重校验/后台刷新 | | 数据静默刷新 —— 动画会分散用户注意力 |
拿不准的时候,就用纯 (默认淡入淡出)或者 。只有当动画需要传达空间含义时,再添加定向动效(上滑、从右侧滑入等)。
<ViewTransition>default="none"Availability
可用性说明
- and
<ViewTransition>requireaddTransitionTypeorreact@canary. They are not in stable React (including 19.x). Before implementing, verify the project uses canary — checkreact@experimentalforpackage.jsonor run"react": "canary". If on stable, install canary:npm ls react.npm install react@canary react-dom@canary - Browser support: Chromium 111+, with Firefox and Safari adding support. The API gracefully degrades — unsupported browsers skip the animation and apply the DOM change instantly.
- 和
<ViewTransition>需要addTransitionType或react@canary版本,不在稳定版 React 中提供(包括 19.x 版本)。实现前请确认项目使用的是 canary 版本:检查react@experimental中是否有package.json,或者执行"react": "canary"查看。如果使用的是稳定版,安装 canary 版本:npm ls react。npm install react@canary react-dom@canary - 浏览器支持:Chromium 111+,Firefox 和 Safari 正在增加支持。API 会优雅降级,不支持的浏览器会跳过动画,直接应用 DOM 变更。
Core Concepts
核心概念
The <ViewTransition>
Component
<ViewTransition><ViewTransition>
组件
<ViewTransition>Wrap the elements you want to animate:
jsx
import { ViewTransition } from 'react';
<ViewTransition>
<Component />
</ViewTransition>React automatically assigns a unique to the nearest DOM node inside each , and calls behind the scenes. Never call yourself — React coordinates all view transitions and will interrupt external ones.
view-transition-name<ViewTransition>document.startViewTransitionstartViewTransition包裹你想要添加动画的元素:
jsx
import { ViewTransition } from 'react';
<ViewTransition>
<Component />
</ViewTransition>React 会自动给每个 内部最近的 DOM 节点分配唯一的 ,并且在底层自动调用 。永远不要自己调用 ,React 会协调所有视图过渡,并且会中断外部触发的过渡。
<ViewTransition>view-transition-namedocument.startViewTransitionstartViewTransitionAnimation Triggers
动画触发器
React decides which type of animation to run based on what changed:
| Trigger | When it fires |
|---|---|
| enter | A |
| exit | A |
| update | DOM mutations happen inside a |
| share | A named |
Only updates wrapped in , , or activate . Regular updates immediately and does not animate.
startTransitionuseDeferredValueSuspense<ViewTransition>setStateReact 会根据变更的内容决定运行哪种类型的动画:
| 触发器 | 触发时机 |
|---|---|
| enter | 过渡过程中 |
| exit | 过渡过程中 |
| update | |
| share | 同一个过渡过程中,一个命名的 |
只有被 、 或 包裹的更新才会激活 。普通的 会立即更新,不会触发动画。
startTransitionuseDeferredValueSuspense<ViewTransition>setStateCritical Placement Rule
关键放置规则
<ViewTransition>jsx
// Works — ViewTransition is before the DOM node
function Item() {
return (
<ViewTransition enter="auto" exit="auto">
<div>Content</div>
</ViewTransition>
);
}
// Broken — a <div> wraps the ViewTransition, preventing enter/exit
function Item() {
return (
<div>
<ViewTransition enter="auto" exit="auto">
<div>Content</div>
</ViewTransition>
</div>
);
}只有当 出现在组件树中所有 DOM 节点之前时,才会激活 enter/exit 动画:
<ViewTransition>jsx
// 正常工作 —— ViewTransition 在 DOM 节点之前
function Item() {
return (
<ViewTransition enter="auto" exit="auto">
<div>Content</div>
</ViewTransition>
);
}
// 无法工作 —— 外层的 <div> 包裹了 ViewTransition,阻止了 enter/exit 触发
function Item() {
return (
<div>
<ViewTransition enter="auto" exit="auto">
<div>Content</div>
</ViewTransition>
</div>
);
}Styling Animations with View Transition Classes
使用视图过渡类自定义动画样式
Props
属性说明
Each prop controls a different animation trigger. Values can be:
- — use the browser default cross-fade
"auto" - — disable this animation type
"none" - — a custom CSS class
"my-class-name" - An object for type-specific animations (see Transition Types below)
{ [transitionType]: value }
jsx
<ViewTransition
default="none" // disable everything not explicitly listed
enter="slide-in" // CSS class for enter animations
exit="slide-out" // CSS class for exit animations
update="cross-fade" // CSS class for update animations
share="morph" // CSS class for shared element animations
/>If is , all triggers are off unless explicitly listed.
default"none"每个属性控制不同的动画触发器,属性值可以是:
- —— 使用浏览器默认的淡入淡出效果
"auto" - —— 禁用该类型的动画
"none" - —— 自定义 CSS 类名
"my-class-name" - 格式为 的对象,用于针对不同过渡类型配置不同动画(见下方过渡类型说明)
{ [transitionType]: value }
jsx
<ViewTransition
default="none" // 禁用所有未明确列出的动画类型
enter="slide-in" // 入场动画使用的 CSS 类
exit="slide-out" // 退场动画使用的 CSS 类
update="cross-fade" // 更新动画使用的 CSS 类
share="morph" // 共享元素动画使用的 CSS 类
/>如果 设置为 ,所有触发器默认关闭,只有明确列出的才会生效。
default"none"Defining CSS Animations
定义 CSS 动画
Use the view transition pseudo-element selectors with the class name:
css
::view-transition-old(.slide-in) {
animation: 300ms ease-out slide-out-to-left;
}
::view-transition-new(.slide-in) {
animation: 300ms ease-out slide-in-from-right;
}
@keyframes slide-out-to-left {
to { transform: translateX(-100%); opacity: 0; }
}
@keyframes slide-in-from-right {
from { transform: translateX(100%); opacity: 0; }
}The pseudo-elements available are:
- — the container for the transition
::view-transition-group(.class) - — contains old and new snapshots
::view-transition-image-pair(.class) - — the outgoing snapshot
::view-transition-old(.class) - — the incoming snapshot
::view-transition-new(.class)
搭配类名使用视图过渡伪元素选择器:
css
::view-transition-old(.slide-in) {
animation: 300ms ease-out slide-out-to-left;
}
::view-transition-new(.slide-in) {
animation: 300ms ease-out slide-in-from-right;
}
@keyframes slide-out-to-left {
to { transform: translateX(-100%); opacity: 0; }
}
@keyframes slide-in-from-right {
from { transform: translateX(100%); opacity: 0; }
}可用的伪元素包括:
- —— 过渡的容器
::view-transition-group(.class) - —— 包含旧快照和新快照
::view-transition-image-pair(.class) - —— 退场元素的快照
::view-transition-old(.class) - —— 入场元素的快照
::view-transition-new(.class)
Transition Types with addTransitionType
addTransitionType使用 addTransitionType
定义过渡类型
addTransitionTypeaddTransitionType<ViewTransition>addTransitionType<ViewTransition>Basic Usage
基础用法
jsx
import { startTransition, addTransitionType } from 'react';
function navigate(url, direction) {
startTransition(() => {
addTransitionType(`navigation-${direction}`); // "navigation-forward" or "navigation-back"
setCurrentPage(url);
});
}You can add multiple types to a single transition, and if multiple transitions are batched, all types are collected.
jsx
import { startTransition, addTransitionType } from 'react';
function navigate(url, direction) {
startTransition(() => {
addTransitionType(`navigation-${direction}`); // "navigation-forward" 或 "navigation-back"
setCurrentPage(url);
});
}你可以给同一个过渡添加多个类型,如果多个过渡被批量处理,所有类型都会被收集。
Using Types with View Transition Classes
结合视图过渡类使用过渡类型
Pass an object instead of a string to any activation prop. Keys are transition type strings, values are CSS class names:
jsx
<ViewTransition
enter={{
'navigation-forward': 'slide-in-from-right',
'navigation-back': 'slide-in-from-left',
default: 'fade-in',
}}
exit={{
'navigation-forward': 'slide-out-to-left',
'navigation-back': 'slide-out-to-right',
default: 'fade-out',
}}
>
<Page />
</ViewTransition>The key inside the object is the fallback when no type matches. If any type has the value , the ViewTransition is disabled for that trigger.
default"none"给触发属性传对象而不是字符串,对象的键是过渡类型字符串,值是 CSS 类名:
jsx
<ViewTransition
enter={{
'navigation-forward': 'slide-in-from-right',
'navigation-back': 'slide-in-from-left',
default: 'fade-in',
}}
exit={{
'navigation-forward': 'slide-out-to-left',
'navigation-back': 'slide-out-to-right',
default: 'fade-out',
}}
>
<Page />
</ViewTransition>对象中的 键是没有匹配到任何类型时的 fallback。如果任意类型的值为 ,该触发器对应的 ViewTransition 会被禁用。
default"none"Using Types with CSS :active-view-transition-type()
:active-view-transition-type()结合 CSS :active-view-transition-type()
使用过渡类型
:active-view-transition-type()React adds transition types as browser view transition types, enabling pure CSS scoping with . Caveat: / match all named elements — the wildcard can override specific class-based animations. Prefer class-based props for per-component animations; reserve for global rules.
:root:active-view-transition-type(type-name)::view-transition-old(*)::view-transition-new(*):active-view-transition-type()The array is also available as the second argument in event callbacks (, , etc.) — see .
typesonEnteronExitreferences/patterns.mdReact 会把过渡类型添加为浏览器视图过渡类型,支持通过 实现纯 CSS 作用域控制。注意: / 会匹配所有命名元素,通配符可能会覆盖特定类的动画。组件级动画优先使用基于类的属性, 保留给全局规则使用。
:root:active-view-transition-type(type-name)::view-transition-old(*)::view-transition-new(*):active-view-transition-type()typesonEnteronExitreferences/patterns.mdTypes and Suspense: When Types Are Available
过渡类型与 Suspense:过渡类型的可用时机
When a with triggers navigation, the transition type is available to all s that enter/exit during that navigation. An outer page-level with a type map sees the type and responds. Inner s with simple string props also enter — the type is irrelevant to them because simple strings fire regardless of type.
<Link>transitionTypes<ViewTransition><ViewTransition><ViewTransition>Subsequent Suspense reveals — when streamed data loads after navigation completes — are separate transitions with no type. This means type-keyed props on Suspense content don't work:
jsx
// This does NOT animate on Suspense reveal — the type is gone by then
<ViewTransition enter={{ "nav-forward": "slide-up", default: "none" }} default="none">
<AsyncContent />
</ViewTransition>When Suspense resolves later, a new transition fires with no type — so applies and nothing animates.
default: "none"Use type maps for s that enter/exit directly with the navigation. Use simple string props for Suspense reveals. See the two-layer pattern in "Two Patterns — Can Coexist with Proper Isolation" below for a complete example.
<ViewTransition>当带有 的 触发导航时,过渡类型对导航过程中所有触发 enter/exit 的 都可用。外层页面级带类型映射的 可以识别到类型并做出响应。使用简单字符串属性的内层 也会正常触发,简单字符串属性不受类型影响,无论什么类型都会生效。
transitionTypes<Link><ViewTransition><ViewTransition><ViewTransition>后续的 Suspense 内容露出(导航完成后流式数据加载完成时)是没有类型的独立过渡。这意味着 Suspense 内容上的类型键属性不会生效:
jsx
// Suspense 内容露出时不会触发动画 —— 此时过渡类型已经消失了
<ViewTransition enter={{ "nav-forward": "slide-up", default: "none" }} default="none">
<AsyncContent />
</ViewTransition>当 Suspense 后续加载完成时,会触发一个新的没有类型的过渡,所以会应用 ,没有任何动画。
default: "none"直接随导航 enter/exit 的 使用类型映射,Suspense 内容露出使用简单字符串属性。 完整示例见下文「两种模式 —— 合理隔离即可共存」中的双层模式。
<ViewTransition>Shared Element Transitions
共享元素过渡
Assign the same to two components — one in the unmounting tree and one in the mounting tree — to animate between them as if they're the same element:
name<ViewTransition>jsx
const HERO_IMAGE = 'hero-image';
function ListView({ onSelect }) {
return (
<ViewTransition name={HERO_IMAGE}>
<img src="/thumb.jpg" onClick={() => startTransition(() => onSelect())} />
</ViewTransition>
);
}
function DetailView() {
return (
<ViewTransition name={HERO_IMAGE}>
<img src="/full.jpg" />
</ViewTransition>
);
}Rules for shared element transitions:
- Only one with a given
<ViewTransition>can be mounted at a time — use globally unique names (namespace with a prefix or module constant).name - The "share" trigger takes precedence over "enter"/"exit".
- If either side is outside the viewport, no pair forms and each side animates independently as enter/exit.
- Use a constant defined in a shared module to avoid name collisions.
给两个 组件分配相同的 —— 一个在卸载的组件树中,一个在挂载的组件树中 —— 就可以实现两个元素之间的过渡,就像它们是同一个元素一样:
<ViewTransition>namejsx
const HERO_IMAGE = 'hero-image';
function ListView({ onSelect }) {
return (
<ViewTransition name={HERO_IMAGE}>
<img src="/thumb.jpg" onClick={() => startTransition(() => onSelect())} />
</ViewTransition>
);
}
function DetailView() {
return (
<ViewTransition name={HERO_IMAGE}>
<img src="/full.jpg" />
</ViewTransition>
);
}共享元素过渡的规则:
- 同一时间只能有一个对应 的
name被挂载 —— 使用全局唯一的名称(加前缀命名空间或者使用模块常量)。<ViewTransition> - 「share」触发器优先级高于「enter」/「exit」。
- 如果任意一侧在视口外,不会形成配对,两侧会分别作为 enter/exit 独立动画。
- 使用共享模块中定义的常量避免名称冲突。
View Transition Events (JavaScript Animations)
视图过渡事件(JavaScript 动画)
For imperative control with , , , callbacks and the object (, , , , ), see . Always return a cleanup function from event handlers. Only one event fires per per Transition — takes precedence over /.
onEnteronExitonUpdateonShareinstance.old.new.group.imagePair.namereferences/patterns.md<ViewTransition>onShareonEnteronExit关于 、、、 回调的命令式控制,以及 对象(包含 、、、、 属性)的使用,详见 。始终要在事件处理器中返回清理函数。每个 每次过渡只会触发一个事件 —— 优先级高于 /。
onEnteronExitonUpdateonShareinstance.old.new.group.imagePair.namereferences/patterns.md<ViewTransition>onShareonEnteronExitCommon Patterns
常用模式
Animate Enter/Exit of a Component
组件入场/退场动画
Conditionally render the itself — toggle with :
<ViewTransition>startTransitionjsx
{show && (
<ViewTransition enter="fade-in" exit="fade-out">
<Panel />
</ViewTransition>
)}有条件地渲染 本身 —— 用 切换显示状态:
<ViewTransition>startTransitionjsx
{show && (
<ViewTransition enter="fade-in" exit="fade-out">
<Panel />
</ViewTransition>
)}Animate List Reorder
列表重排动画
Wrap each item (not a wrapper div) in with a stable :
<ViewTransition>keyjsx
{items.map(item => (
<ViewTransition key={item.id}>
<ItemCard item={item} />
</ViewTransition>
))}Triggering the reorder inside will smoothly animate each item to its new position. Avoid wrapper s between the list and — they block the reorder animation.
startTransition<div><ViewTransition>How it works: doesn't need async work to animate. The View Transition API captures a "before" snapshot of the DOM, then React applies the state update, and the API captures an "after" snapshot. As long as items change position between snapshots, the animation runs — even for purely synchronous local state changes like sorting.
startTransition给每个元素(不是外层容器 div)包裹 ,并使用稳定的 :
<ViewTransition>keyjsx
{items.map(item => (
<ViewTransition key={item.id}>
<ItemCard item={item} />
</ViewTransition>
))}在 内部触发重排时,每个元素会平滑地动画到新位置。避免在列表和 之间加外层 —— 它们会阻塞重排动画。
startTransition<ViewTransition><div>工作原理: 不需要异步工作也能触发动画。View Transition API 会捕获 DOM 的「变更前」快照,然后 React 应用状态更新,API 再捕获「变更后」快照。只要元素在两个快照之间位置发生了变化,动画就会运行 —— 即使是纯同步的本地状态变更比如排序也可以。
startTransitionForce Re-Enter with key
key使用 key
强制重新入场
keyUse a prop on to force an enter/exit animation when a value changes — even if the component itself doesn't unmount:
key<ViewTransition>jsx
<ViewTransition key={searchParams.toString()} enter="slide-up" exit="slide-down" default="none">
<ResultsGrid results={results} />
</ViewTransition>When the key changes, React unmounts and remounts the , which triggers exit on the old instance and enter on the new one. This is useful for animating content swaps driven by URL parameters, tab switches, or any state change where the content identity changes but the component type stays the same.
<ViewTransition>Caution with Suspense: If the wraps a , changing the key remounts the entire Suspense boundary, re-triggering the data fetch. Only use on outside of Suspense, or accept the refetch.
<ViewTransition><Suspense>key<ViewTransition>给 添加 属性,当值变化时强制触发入场/退场动画 —— 即使组件本身没有卸载:
<ViewTransition>keyjsx
<ViewTransition key={searchParams.toString()} enter="slide-up" exit="slide-down" default="none">
<ResultsGrid results={results} />
</ViewTransition>当 key 变化时,React 会卸载并重新挂载 ,触发旧实例的 exit 动画和新实例的 enter 动画。这适用于 URL 参数驱动的内容切换、标签页切换,或者任何内容标识变化但组件类型不变的状态变更场景的动画。
<ViewTransition>Suspense 注意事项: 如果 包裹了 ,修改 key 会重新挂载整个 Suspense 边界,重新触发数据请求。只在 Suspense 外部的 上使用 ,或者接受重新请求的行为。
<ViewTransition><Suspense><ViewTransition>keyAnimate Suspense Fallback to Content
Suspense fallback 到内容的过渡动画
The simplest approach: wrap in a single for a zero-config cross-fade from skeleton to content:
<Suspense><ViewTransition>jsx
<ViewTransition>
<Suspense fallback={<Skeleton />}>
<Content />
</Suspense>
</ViewTransition>For directional motion, give the fallback and content separate s. Use on the content to prevent re-animation on revalidation:
<ViewTransition>default="none"jsx
<Suspense
fallback={
<ViewTransition exit="slide-down">
<Skeleton />
</ViewTransition>
}
>
<ViewTransition default="none" enter="slide-up">
<AsyncContent />
</ViewTransition>
</Suspense>Why on the fallback and on the content? When Suspense resolves, two things happen simultaneously in one transition: the fallback unmounts (exit) and the content mounts (enter). The fallback slides down and fades out while the content slides up and fades in — creating a smooth handoff. The staggered CSS timing ( delays by the duration) ensures the skeleton leaves before new content arrives.
exitenterenterexit最简单的实现方式:给 包裹一层 ,实现骨架屏到内容的零配置淡入淡出效果:
<Suspense><ViewTransition>jsx
<ViewTransition>
<Suspense fallback={<Skeleton />}>
<Content />
</Suspense>
</ViewTransition>如果需要定向动效,给 fallback 和内容分别添加 。给内容添加 避免重校验时重复动画:
<ViewTransition>default="none"jsx
<Suspense
fallback={
<ViewTransition exit="slide-down">
<Skeleton />
</ViewTransition>
}
>
<ViewTransition default="none" enter="slide-up">
<AsyncContent />
</ViewTransition>
</Suspense>为什么给 fallback 加 ,给内容加 ? 当 Suspense 加载完成时,同一个过渡中会同时发生两件事:fallback 卸载(触发 exit)和内容挂载(触发 enter)。Fallback 下滑淡出的同时内容上滑淡入,实现平滑的交接。错开的 CSS 时序( 延迟 的时长)可以确保骨架屏在新内容到达前完全离开。
exitenterenterexitOpt Out of Nested Animations
退出嵌套动画
Wrap children in to prevent them from animating when a parent changes:
<ViewTransition update="none">jsx
<ViewTransition>
<div className={theme}>
<ViewTransition update="none">
{children}
</ViewTransition>
</div>
</ViewTransition>For more patterns (isolate persistent/floating elements, reusable animated collapse, preserve state with , exclude elements with ), see .
<Activity>useOptimisticreferences/patterns.md给子元素包裹 ,避免父元素变化时子元素也跟着触发动画:
<ViewTransition update="none">jsx
<ViewTransition>
<div className={theme}>
<ViewTransition update="none">
{children}
</ViewTransition>
</div>
</ViewTransition>更多模式(隔离持久/悬浮元素、可复用的动画折叠组件、使用 保留状态、使用 排除元素)详见 。
<Activity>useOptimisticreferences/patterns.mdHow Multiple <ViewTransition>
s Interact
<ViewTransition>多个 <ViewTransition>
的交互逻辑
<ViewTransition>When a transition fires, every in the tree that matches the trigger participates simultaneously. Each gets its own , and the browser animates all of them inside a single call. They run in parallel, not sequentially.
<ViewTransition>view-transition-namedocument.startViewTransitionThis means multiple s that fire during the same transition all animate at once. A layout-level cross-fade + a page-level slide-up + per-item reorder all running in the same produces competing animations. But s that fire in different transitions (e.g., navigation vs. a later Suspense resolve) don't compete — they animate at different moments.
<ViewTransition>document.startViewTransition<ViewTransition>当过渡触发时,组件树中所有匹配触发器的 会同时参与。每个都会获得自己的 ,浏览器会在同一个 调用中给所有元素添加动画。它们是并行运行的,不是串行的。
<ViewTransition>view-transition-namedocument.startViewTransition这意味着在同一个过渡中触发的多个 会同时动画。布局级的淡入淡出 + 页面级的上滑动画 + 元素重排动画都在同一个 中运行,会导致动画互相冲突。但在不同过渡中触发的 (比如导航 vs 后续的 Suspense 加载完成)不会冲突 —— 它们会在不同的时间点动画。
<ViewTransition>document.startViewTransition<ViewTransition>Use default="none"
Liberally
default="none"多使用 default="none"
default="none"Prevent unintended animations by disabling the default trigger on ViewTransitions that should only fire for specific types:
jsx
// Only animates when 'navigation-forward' or 'navigation-back' types are present.
// Silent on all other transitions (Suspense reveals, state changes, etc.)
<ViewTransition
default="none"
enter={{
'navigation-forward': 'slide-in-from-right',
'navigation-back': 'slide-in-from-left',
default: 'none',
}}
exit={{
'navigation-forward': 'slide-out-to-left',
'navigation-back': 'slide-out-to-right',
default: 'none',
}}
>
{children}
</ViewTransition>TypeScript note: When passing an object to /, the type requires a key. Always include (or ) in the object — omitting it causes a type error even if the component-level prop is set.
enterexitViewTransitionClassPerTypedefaultdefault: 'none''auto'defaultWithout , a with (the implicit default) fires the browser's cross-fade on every transition — including ones triggered by child Suspense boundaries, updates, or calls within the page.
default="none"<ViewTransition>default="auto"useDeferredValuestartTransitionNext.js revalidation: This is especially important in Next.js — when fires (from a Server Action, webhook, or polling), the page re-renders. Without , every in the tree re-animates: content slides up again, things flash. Always use on content s and only enable specific triggers (, ) explicitly.
revalidateTag()default="none"<ViewTransition>default="none"<ViewTransition>enterexit给只需要针对特定类型触发的 ViewTransition 禁用默认触发器,避免意外动画:
jsx
// 只有当 'navigation-forward' 或 'navigation-back' 类型存在时才会动画
// 其他所有过渡(Suspense 露出、状态变更等)都静默
<ViewTransition
default="none"
enter={{
'navigation-forward': 'slide-in-from-right',
'navigation-back': 'slide-in-from-left',
default: 'none',
}}
exit={{
'navigation-forward': 'slide-out-to-left',
'navigation-back': 'slide-out-to-right',
default: 'none',
}}
>
{children}
</ViewTransition>TypeScript 注意: 给 / 传对象时, 类型要求必须有 键。始终要在对象中包含 (或 )—— 即使组件级的 属性已经设置了,省略它也会导致类型错误。
enterexitViewTransitionClassPerTypedefaultdefault: 'none''auto'default如果不设置 ,默认 的 会在每一次过渡中都触发浏览器的淡入淡出动画 —— 包括子 Suspense 边界触发的更新、 更新,或者页面内部的 调用。
default="none"default="auto"<ViewTransition>useDeferredValuestartTransitionNext.js 重校验场景: 这在 Next.js 中尤其重要 —— 当 触发时(来自服务端动作、webhook 或轮询),页面会重新渲染。如果没有 ,组件树中所有的 都会重新动画:内容再次上滑、元素闪烁。始终给内容 添加 ,只显式启用特定的触发器(、)。
revalidateTag()default="none"<ViewTransition><ViewTransition>default="none"enterexitTwo Patterns — Can Coexist with Proper Isolation
两种模式 —— 合理隔离即可共存
There are two distinct view transition patterns:
Pattern A — Directional page slides (e.g., left/right navigation):
- Uses on
transitionTypesor<Link>to tag navigation directionaddTransitionType - An outer on the page maps types to slide classes with
<ViewTransition>default="none" - Fires during the navigation transition (when the type is present)
Pattern B — Suspense content reveals (e.g., streaming data):
- No needed
transitionTypes - Simple /
enter="slide-up"onexit="slide-down"s around Suspense boundaries<ViewTransition> - prevents re-animation on revalidation
default="none" - Fires later when data loads (a separate transition with no type)
These coexist when they fire at different moments. The nav slide fires during the navigation transition (with the type); the Suspense reveal fires later when data streams in (no type). on both layers prevents cross-interference — the nav VT ignores Suspense resolves, and the Suspense VT ignores navigations:
default="none"jsx
<ViewTransition
enter={{ "nav-forward": "slide-from-right", default: "none" }}
exit={{ "nav-forward": "slide-to-left", default: "none" }}
default="none"
>
<div>
<Suspense fallback={
<ViewTransition exit="slide-down"><Skeleton /></ViewTransition>
}>
<ViewTransition enter="slide-up" default="none">
<Content />
</ViewTransition>
</Suspense>
</div>
</ViewTransition>Always pair with on directional transitions. Without an exit animation, the old page disappears instantly while the new one slides in at scroll position 0 — a jarring jump. The exit slide masks the scroll change within the transition snapshot because the old content animates out simultaneously.
enterexitWhen they DO conflict: If both layers use , or if a layout-level fires a cross-fade during the same transition as a page-level slide-up, they animate simultaneously and fight for attention. The conflict is about same-moment animations, not about using both patterns on the same page.
default="auto"<ViewTransition>Place the outer directional in each page component — not in a layout (layouts persist and don't trigger enter/exit). Per-page wrappers are the cleanest approach.
<ViewTransition>Shared element transitions ( prop) work alongside either pattern because the trigger takes precedence over /.
nameshareenterexit有两种不同的视图过渡模式:
模式 A —— 定向页面滑动(比如左右导航):
- 在 上使用
<Link>或者transitionTypes标记导航方向addTransitionType - 页面外层的 把类型映射为滑动类,设置
<ViewTransition>default="none" - 在导航过渡期间触发(此时过渡类型存在)
模式 B —— Suspense 内容露出(比如流式数据加载):
- 不需要
transitionTypes - 给 Suspense 边界周围的 设置简单的
<ViewTransition>/enter="slide-up"exit="slide-down" - 避免重校验时重复动画
default="none" - 数据加载完成后触发(没有类型的独立过渡)
当它们在不同时间触发时可以共存。 导航滑动在导航过渡期间触发(有类型);Suspense 内容露出在后续数据流式加载完成时触发(无类型)。两层都设置 避免交叉干扰 —— 导航 VT 忽略 Suspense 加载完成事件,Suspense VT 忽略导航事件:
default="none"jsx
<ViewTransition
enter={{ "nav-forward": "slide-from-right", default: "none" }}
exit={{ "nav-forward": "slide-to-left", default: "none" }}
default="none"
>
<div>
<Suspense fallback={
<ViewTransition exit="slide-down"><Skeleton /></ViewTransition>
}>
<ViewTransition enter="slide-up" default="none">
<Content />
</ViewTransition>
</Suspense>
</div>
</ViewTransition>定向过渡中始终要配对 和 。 如果没有 exit 动画,旧页面会瞬间消失,新页面在滚动位置0滑入 —— 会有突兀的跳转。退出滑动可以在过渡快照中掩盖滚动变化,因为旧内容会同时动画退出。
enterexit什么时候会冲突: 如果两层都使用 ,或者布局级的 和页面级的上滑动画在同一个过渡中触发淡入淡出,它们会同时动画,互相干扰。冲突是因为同时刻的动画,而不是因为在同一个页面使用两种模式。
default="auto"<ViewTransition>把外层定向 放在每个页面组件中 —— 不要放在布局里(布局是持久的,不会触发 enter/exit)。每个页面单独包裹是最简洁的实现方式。
<ViewTransition>共享元素过渡( 属性)可以和任意模式共存,因为 触发器优先级高于 /。
nameshareenterexitNext.js Integration
Next.js 集成
Next.js supports React View Transitions. works out of the box for - and -triggered updates — no config needed.
<ViewTransition>startTransitionSuspenseTo also animate navigations, enable the experimental flag in (or ):
<Link>next.config.jsnext.config.tsjs
const nextConfig = {
experimental: {
viewTransition: true,
},
};
module.exports = nextConfig;What this flag does: It wraps every navigation in , so all mounted components participate in every link click. Without this flag, only /-triggered transitions animate. This makes the composition rules in "How Multiple s Interact" especially important: use on layout-level s to avoid competing animations.
<Link>document.startViewTransition<ViewTransition>startTransitionSuspense<ViewTransition>default="none"<ViewTransition>For a detailed guide including App Router patterns and Server Component considerations, see .
references/nextjs.mdKey points:
- The component is imported from
<ViewTransition>directly — no Next.js-specific import.react - Works with the App Router and +
startTransitionfor programmatic navigation.router.push()
Next.js 支持 React View Transitions。 开箱即支持 和 触发的更新 —— 不需要额外配置。
<ViewTransition>startTransitionSuspense如果要给 导航也添加动画,需要在 (或 )中启用实验性 flag:
<Link>next.config.jsnext.config.tsjs
const nextConfig = {
experimental: {
viewTransition: true,
},
};
module.exports = nextConfig;这个 flag 的作用: 它会把每个 导航都包裹在 中,所以所有挂载的 组件都会参与每次链接点击的过渡。如果没有这个 flag,只有 / 触发的过渡才会动画。这让「多个 的交互逻辑」部分的组合规则尤其重要:给布局级 使用 避免动画冲突。
<Link>document.startViewTransition<ViewTransition>startTransitionSuspense<ViewTransition><ViewTransition>default="none"包含 App Router 模式和服务端组件注意事项的详细指南见 。
references/nextjs.md关键点:
- 组件直接从
<ViewTransition>导入 —— 不需要 Next.js 特定的导入。react - 支持 App Router 模式,以及编程式导航的 +
startTransition用法。router.push()
The transitionTypes
prop on next/link
transitionTypesnext/linknext/link
的 transitionTypes
属性
next/linktransitionTypesnext/linktransitionTypes'use client'tsx
<Link href="/products/1" transitionTypes={['transition-to-detail']}>View Product</Link>For full examples with shared element transitions and directional animations, see .
references/nextjs.mdnext/linktransitionTypes'use client'tsx
<Link href="/products/1" transitionTypes={['transition-to-detail']}>查看商品</Link>包含共享元素过渡和定向动画的完整示例见 。
references/nextjs.mdAccessibility
可访问性
Always respect . React does not disable animations automatically for this preference. Add this to your global CSS:
prefers-reduced-motioncss
@media (prefers-reduced-motion: reduce) {
::view-transition-old(*),
::view-transition-new(*),
::view-transition-group(*) {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
}Or disable specific animations conditionally in JavaScript events by checking the media query.
始终要尊重 设置。React 不会自动针对该偏好禁用动画。在全局 CSS 中添加以下代码:
prefers-reduced-motioncss
@media (prefers-reduced-motion: reduce) {
::view-transition-old(*),
::view-transition-new(*),
::view-transition-group(*) {
animation-duration: 0s !important;
animation-delay: 0s !important;
}
}或者在 JavaScript 事件中通过检查媒体查询有条件地禁用特定动画。
Reference Files
参考文件
- — Real-world patterns (searchable grids, expand/collapse, type-safe helpers), animation timing, view transition events (JavaScript Animations API), and troubleshooting.
references/patterns.md - — Ready-to-use CSS animation recipes (slide, fade, scale, directional nav, and combined patterns).
references/css-recipes.md - — Detailed Next.js integration guide with App Router patterns and Server Component considerations.
references/nextjs.md
- —— 真实场景的模式(可搜索网格、展开/折叠、类型安全的辅助工具)、动画时序、视图过渡事件(JavaScript Animations API)以及问题排查。
references/patterns.md - —— 开箱即用的 CSS 动画模板(滑动、淡入淡出、缩放、定向导航以及组合模式)。
references/css-recipes.md - —— 详细的 Next.js 集成指南,包含 App Router 模式和服务端组件注意事项。",
references/nextjs.md