12 KiB
name, description, license, metadata
| name | description | license | metadata | ||||
|---|---|---|---|---|---|---|---|
| vercel-react-view-transitions | 使用 React 视图过渡 API(`<ViewTransition>` 组件、`addTransitionType` 和 CSS 视图过渡伪元素)实现流畅、原生感动画的指南。当用户希望添加页面过渡、动画化路由变化、创建共享元素动画、动画化组件进入/退出、动画化列表重排、实现方向性(前进/后退)导航动画,或在 Next.js 中集成视图过渡时,应使用此技能。当用户提及视图过渡、`startViewTransition`、`ViewTransition`、过渡类型,或询问如何在 React 中不使用第三方动画库来实现 UI 状态间的动画时,也应使用此技能。 | MIT |
|
React 视图过渡
使用浏览器原生 document.startViewTransition 在 UI 状态之间进行动画。用 <ViewTransition> 声明做什么,用 startTransition / useDeferredValue / Suspense 触发何时做,用 CSS 类控制如何做。不支持的浏览器会优雅地跳过动画。
何时使用动画
每个 <ViewTransition> 都应该传达一种空间关系或连续性。如果你说不清它传达了什么,就不要添加它。
按顺序实现此列表中所有适用的模式:
| 优先级 | 模式 | 传达的内容 |
|---|---|---|
| 1 | 共享元素(name) |
"同一个东西——正在深入" |
| 2 | Suspense 揭示 | "数据已加载" |
| 3 | 列表标识(每个项目 key) |
"相同项目,新的排列" |
| 4 | 状态变化(enter/exit) |
"某物出现/消失了" |
| 5 | 路由变化(布局层级) | "前往新位置" |
这是一个实现顺序,不是"选其一"的列表。实现适用于应用的每一种模式。只有当应用没有该模式的使用场景时才跳过。
选择动画风格
| 上下文 | 动画 | 原因 |
|---|---|---|
| 层级导航(列表 → 详情) | 类型键控 nav-forward / nav-back |
传达空间深度 |
| 横向导航(标签页间切换) | 裸 <ViewTransition>(淡入淡出)或 default="none" |
没有需要传达的深度 |
| Suspense 揭示 | enter/exit 字符串属性 |
内容到达 |
| 重新验证/后台刷新 | default="none" |
静默——无需动画 |
将方向性滑动保留给层级导航(列表 → 详情)和有序序列(上一张/下一张照片、轮播、分页结果)。对于有序序列,方向传达位置:"下一张"从右侧滑入,"上一张"从左侧滑入。横向/无序导航(标签页间切换)不应使用方向性滑动——这会错误地暗示空间深度。
可用性
- Next.js: 不要安装
react@canary——App Router 已在内部打包了 React canary。ViewTransition开箱即用。npm ls react可能显示一个看起来是稳定版的版本;这是预期行为。 - 不使用 Next.js: 安装
react@canary react-dom@canary(ViewTransition不在稳定版 React 中)。 - 浏览器支持:Chromium 111+、Firefox 144+、Safari 18.2+。在不支持的浏览器上优雅降级。
实现工作流程
在为现有应用添加视图过渡时,严格按照 references/implementation.md 的步骤进行操作。 从审计开始——不要跳过。将 references/css-recipes.md 中的 CSS 配方复制到全局样式表中——不要自己编写动画 CSS。
核心概念
<ViewTransition> 组件
import { ViewTransition } from 'react';
<ViewTransition>
<Component />
</ViewTransition>
React 会自动分配一个唯一的 view-transition-name 并在后台调用 document.startViewTransition。永远不要自己调用 startViewTransition。
动画触发器
| 触发器 | 触发时机 |
|---|---|
| enter | 在过渡期间首次插入 <ViewTransition> |
| exit | 在过渡期间首次移除 <ViewTransition> |
| update | <ViewTransition> 内部的 DOM 发生变化。嵌套 VT 的情况下,变化应用于最内层 |
| share | 命名 VT 卸载,且同一过渡中另一个同名 VT 挂载 |
只有 startTransition、useDeferredValue 或 Suspense 能激活 VT。普通的 setState 不会触发动画。
关键放置规则
<ViewTransition> 只有在出现在任何 DOM 节点之前才能激活 enter/exit:
// 有效
<ViewTransition enter="auto" exit="auto">
<div>内容</div>
</ViewTransition>
// 无效——div 包裹了 VT,抑制了 enter/exit
<div>
<ViewTransition enter="auto" exit="auto">
<div>内容</div>
</ViewTransition>
</div>
使用视图过渡类设置样式
属性
取值:"auto"(浏览器交叉淡入淡出)、"none"(禁用)、"class-name"(自定义 CSS),或 { [type]: value } 用于类型特定的动画。
<ViewTransition default="none" enter="slide-in" exit="slide-out" share="morph" />
如果 default 为 "none",则除非明确列出,否则所有触发器都关闭。
CSS 伪元素
::view-transition-old(.class)——旧快照::view-transition-new(.class)——新快照::view-transition-group(.class)——容器::view-transition-image-pair(.class)——新旧配对
参见 references/css-recipes.md 获取可直接使用的动画配方。
过渡类型
使用 addTransitionType 标记过渡,以便 VT 可以根据上下文选择不同的动画。多次调用可叠加类型——树中不同的 VT 对不同的类型作出响应:
startTransition(() => {
addTransitionType('nav-forward');
addTransitionType('select-item');
router.push('/detail/1');
});
传入对象可将类型映射到 CSS 类。适用于 enter、exit和 share:
<ViewTransition
enter={{ 'nav-forward': 'slide-from-right', 'nav-back': 'slide-from-left', default: 'none' }}
exit={{ 'nav-forward': 'slide-to-left', 'nav-back': 'slide-to-right', default: 'none' }}
share={{ 'nav-forward': 'morph-forward', 'nav-back': 'morph-back', default: 'morph' }}
default="none"
>
<Page />
</ViewTransition>
enter 和 exit 不必对称。例如,淡入但按方向滑出:
<ViewTransition
enter={{ 'nav-forward': 'fade-in', 'nav-back': 'fade-in', default: 'none' }}
exit={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
default="none"
>
TypeScript: ViewTransitionClassPerType 在对象中需要一个 default 键。
对于有多个页面的应用,将类型键控 VT 提取为可复用的包装组件:
export function DirectionalTransition({ children }: { children: React.ReactNode }) {
return (
<ViewTransition
enter={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
exit={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
default="none"
>
{children}
</ViewTransition>
);
}
router.back() 和浏览器返回按钮
router.back() 以及浏览器的前进/后退按钮不会触发视图过渡(popstate 是同步的,与 startViewTransition 不兼容)。应改用带显式 URL 的 router.push()。
类型与 Suspense
类型在导航期间可用,但在后续的 Suspense 揭示期间不可用(这是独立的过渡,没有类型)。页面级 enter/exit 使用类型映射;Suspense 揭示使用简单的字符串属性。
共享元素过渡
两个 VT 上使用相同的 name——一个卸载,一个挂载——会创建共享元素变形效果:
<ViewTransition name="hero-image">
<img src="/thumb.jpg" onClick={() => startTransition(() => onSelect())} />
</ViewTransition>
// 在另一个视图中——同名
<ViewTransition name="hero-image">
<img src="/full.jpg" />
</ViewTransition>
- 同一时刻只能挂载一个具有给定
name的 VT——请使用唯一名称(photo-${id})。注意可复用组件:如果一个带有命名 VT 的组件同时渲染在模态框/弹出窗口和页面中,两者会同时挂载并破坏变形效果。要么使名称成为条件(通过属性传入),要么将命名 VT 移出共享组件,放入特定的消费者中。 share优先于enter/exit。仔细考虑每个导航路径:当没有形成匹配对时(例如目标页面没有相同的名称),会触发enter/exit。考虑该元素是否需要在那些路径上有回退动画。- 永远不要对带有共享变形效果的页面使用淡出退出——应使用方向性滑动。
常见模式
进入/退出
{show && (
<ViewTransition enter="fade-in" exit="fade-out"><Panel /></ViewTransition>
)}
列表重排
{items.map(item => (
<ViewTransition key={item.id}><ItemCard item={item} /></ViewTransition>
))}
在 startTransition 内部触发。避免在列表和 VT 之间使用包装 <div>。
组合共享元素与列表标识
共享元素和列表标识是独立的关注点——不要将两者混淆。当一个列表项包含共享元素时(例如,一张图片变形为详情视图),使用两个嵌套的 <ViewTransition> 边界:
{items.map(item => (
<ViewTransition key={item.id}> {/* 列表标识 */}
<Link href={`/items/${item.id}`}>
<ViewTransition name={`item-image-${item.id}`} share="morph"> {/* 共享元素 */}
<Image src={item.image} />
</ViewTransition>
<p>{item.name}</p>
</Link>
</ViewTransition>
))}
外层 VT 处理列表重排/进入动画。内层 VT 处理跨路由的共享元素变形。缺少任何一层,该动画都会静默地不执行。
使用 key 强制重新进入
<ViewTransition key={searchParams.toString()} enter="slide-up" default="none">
<ResultsGrid />
</ViewTransition>
注意: 如果包裹 <Suspense>,更改 key 会重新挂载边界并重新获取数据。
Suspense 回退到内容
简单的交叉淡入淡出:
<ViewTransition>
<Suspense fallback={<Skeleton />}><Content /></Suspense>
</ViewTransition>
方向性揭示:
<Suspense fallback={<ViewTransition exit="slide-down"><Skeleton /></ViewTransition>}>
<ViewTransition enter="slide-up" default="none"><Content /></ViewTransition>
</Suspense>
更多模式参见 references/patterns.md。
多个 VT 如何交互
每个匹配触发器的 VT 会在一次 document.startViewTransition 中同时触发。不同过渡(导航 vs 后续的 Suspense 解析)中的 VT 不会相互竞争。
广泛使用 default="none"
否则,每个 VT 都会在每次过渡上触发浏览器交叉淡入淡出——Suspense 解析、useDeferredValue 更新、后台重新验证。始终使用 default="none" 并仅显式启用所需的触发器。
两种模式共存
模式 A——方向性滑动: 每个页面上的类型键控 VT,在导航期间触发。 模式 B——Suspense 揭示: 简单的字符串属性,在数据加载时触发(无类型)。
它们可以共存,因为它们在不同的时机触发。两者上的 default="none" 防止交叉干扰。始终将 enter 与 exit 配对。将方向性 VT 放在页面组件中,而不是布局中。
嵌套 VT 限制
当父 VT 退出时,其内部的嵌套 VT 不会触发自己的 enter/exit——只有最外层的 VT 会进行动画。页面导航期间按项目交错动画目前无法实现。参见 react#36135 了解实验性的选择性加入修复。
Next.js 集成
有关 Next.js 的设置(experimental.viewTransition 标志、next/link 上的 transitionTypes 属性、App Router 模式、服务器组件),参见 references/nextjs.md。
无障碍
始终将 references/css-recipes.md 中的减少动画 CSS 添加到你的全局样式表中。
参考文件
references/implementation.md——逐步实现工作流程。references/patterns.md——模式、动画时序、事件 API、故障排除。references/css-recipes.md——可直接使用的 CSS 动画配方。references/nextjs.md——Next.js App Router 模式和服务器组件详情。
完整编译文档
包含所有参考文件展开的完整指南:AGENTS.md