关于 vercel-react-view-transitions
这项技能指导开发者在 React 应用中使用 View Transitions,并按界面关系选择动画模式。内容涵盖实现流程、浏览器支持及无障碍注意事项。
主要功能
- 01共享元素过渡
- 02Suspense 内容显现动画
- 03列表与状态过渡
- 04CSS 过渡样式
- 05浏览器支持指南
- 06Next.js 集成指南
适用场景
- 01为列表到详情页的导航添加动画
- 02显示 Suspense 加载完成的内容
- 03为重排或变化的列表添加动画
- 04为现有 React 应用添加过渡
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vercel-labs/ agent-skills --skill vercel-react-view-transitions安装后在 Claude Code 中输入/vercel-react-view-transitions确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
AGENTS.mdmetadata.jsonREADME.mdreferences/css-recipes.mdimplementation.mdnextjs.mdpatterns.mdtroubleshooting.md详细介绍
原文在翻译后有更新,译文可能不是最新。
React View Transitions
使用浏览器原生的 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(稳定版 React 中没有ViewTransition)。 - 浏览器支持:Chromium 125+(React 需要
startViewTransition的 v2 对象形式)、Firefox 144+、Safari 18.2+。不支持的浏览器会优雅降级。
实现工作流
为现有应用添加视图过渡时,请逐步遵循 references/implementation.md。 从审查开始——不要跳过。使用 references/css-recipes.md 中适用的 CSS,并根据应用进行调整。
核心概念
<ViewTransition> 组件
import { ViewTransition } from 'react';
<ViewTransition>
<Component />
</ViewTransition>
React 会自动分配唯一的 view-transition-name,并在幕后调用 document.startViewTransition。不要自行调用 startViewTransition。
动画触发条件
| 触发条件 | 触发时机 |
|---|---|
| enter | <ViewTransition> 在 Transition 期间首次插入 |
| exit | <ViewTransition> 在 Transition 期间首次移除 |
| update | <ViewTransition> 内部的 DOM 发生变化,或者边界因相邻的直接兄弟节点而改变尺寸/位置。嵌套 VT 中,变化由最内层的 VT 应用 |
| share | 命名 VT 卸载,且同一 Transition 中挂载了另一个具有相同 name 的 VT |
只有 startTransition、useDeferredValue 或 Suspense 才会激活 VT。常规的 setState 不会触发动画。
关键放置规则
只有当 <ViewTransition> 出现在任何 DOM 节点之前时,才会激活进入/退出动画:
// Works
<ViewTransition enter="auto" exit="auto">
<div>Content</div>
</ViewTransition>
// Broken — div wraps the VT, suppressing enter/exit
<div>
<ViewTransition enter="auto" exit="auto">
<div>Content</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() 以及浏览器的前进/后退按钮不会携带转场类型,因此按类型指定的动画(方向性滑动)会解析为 default,不会播放——未指定类型的共享元素变形仍会应用。对于按类型指定的动画,请使用带有明确 URL 的 router.push()。
类型与 Suspense
类型在导航期间可用,但在后续 Suspense 内容显示时不可用(这是单独的转场,不带类型)。页面级别的入场/离场请使用类型映射;Suspense 内容显示请使用简单的字符串属性。
共享元素就绪状态
只有旧视图和新视图在同一个 Transition 中渲染时,共享元素转场才能配对元素。如果传入内容处于挂起状态,本次更新中只有其回退内容存在;已解析的内容会在后续的 Suspense 转场中显示,可单独设置动画。
共享元素转场
两个 VT 使用相同的 name,一个卸载、一个挂载,即可创建共享元素变形:
<ViewTransition name="hero-image">
<img src="/thumb.jpg" onClick={() => startTransition(() => onSelect())} />
</ViewTransition>
// On the other view — same name
<ViewTransition name="hero-image">
<img src="/full.jpg" />
</ViewTransition>
- 同一时间只能挂载一个具有给定
name的 VT——请使用唯一名称(photo-${id})。注意可复用组件:如果同时在模态框/弹出层和页面中渲染带命名 VT 的组件,两者会同时挂载,导致变形失效。可通过属性设置条件名称,或将命名 VT 从共享组件移出,放到具体的使用方中。 share的优先级高于enter/exit。请仔细考虑每种导航路径:未形成匹配对时(例如目标页面没有相同的名称),会改为触发enter/exit。请考虑元素是否需要在这些路径下使用回退动画。- 有两种情况会导致已接入的变形静默失效:(1)设置了
default="none",但没有显式设置share属性——share会解析为none;(2)按类型指定了share,但导航未添加相应类型——普通链接点击会解析为映射中的default。所有需要执行变形的链接都必须添加类型(在next/link上设置transitionTypes,或使用addTransitionType)。 - 含有共享变形的页面上,切勿使用淡出式离场动画——请改用方向性滑动。
常见模式
入场/离场
{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> 中,使其滑动而非跳跃。请参阅布局位移变形。
使用列表标识组合共享元素
共享元素和列表标识是彼此独立的问题——不要混为一谈。当列表项中包含共享元素(例如,变形为详情视图的图片)时,请使用两个嵌套的 <ViewTransition> 边界:
{items.map(item => (
<ViewTransition key={item.id}> {/* list identity */}
<Link href={`/items/${item.id}`}>
<ViewTransition name={`item-image-${item.id}`} share="morph"> {/* shared element */}
<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 中同时触发。发生在不同过渡中的 VT(导航与之后 Suspense 解析)不会相互竞争。
谨慎使用 default="none"
不使用它时,每个 VT 都会在每次过渡中触发浏览器交叉淡入淡出——包括 Suspense 解析、useDeferredValue 更新和后台重新验证。在命名/共享元素和按类型标识的页面 VT 上使用 default="none"。
但它也会关闭 update(布局/重排变形)和 share(没有显式 share 属性的命名元素对永远不会变形)。带键的列表项和移位的同级元素需要 update——让它们保持默认设置,或设为 update="auto"。
两种模式可以共存
模式 A——方向性滑动: 每个页面上的按类型标识 VT,在导航期间触发。 模式 B——Suspense 揭示: 简单的字符串属性,在数据加载时触发(不使用类型)。
它们可以共存,因为触发时机不同。在两者上都设置 default="none" 可防止相互干扰。始终将 enter 与 exit 配对。将方向性 VT 放在页面组件中,而不是布局中。
嵌套 VT 的限制
当父 VT 与其中的嵌套 VT 作为一个整体挂载/卸载时,嵌套 VT 不会触发自己的 enter/exit——只有最外层的 VT 会执行动画。(如果子 VT 挂载在一个持续存在的父 VT 中,则会正常触发 enter/exit。)目前 Next.js 不支持页面导航期间逐项错开动画;上游实验状态请参阅故障排查。
Next.js 集成
有关 Next.js 集成(next/link 和 useRouter 上的 transitionTypes、App Router 模式、Server Components),请参阅 references/nextjs.md。
无障碍
请务必将 references/css-recipes.md 中的减少动态效果 CSS 添加到全局样式表。
参考文件
- references/implementation.md — 分步实现工作流。
- references/patterns.md — 模式、动画时序和事件 API。
- references/troubleshooting.md — 按症状排查问题和运行时限制。
- references/css-recipes.md — 可直接使用的 CSS 动画方案。
- references/nextjs.md — Next.js App Router 模式和 Server Component 详情。
完整编译文档
包含所有展开的参考文件的完整指南:AGENTS.md