mcpField
V
vercel-react-view-transitions

vercel-react-view-transitions

官方
github.com/vercel-labs设计与 UI33.2k171.8k 次安装1 天前更新
代码仓库

关于 vercel-react-view-transitions

这项技能指导开发者在 React 应用中使用 View Transitions,并按界面关系选择动画模式。内容涵盖实现流程、浏览器支持及无障碍注意事项。

主要功能

  1. 01共享元素过渡
  2. 02Suspense 内容显现动画
  3. 03列表与状态过渡
  4. 04CSS 过渡样式
  5. 05浏览器支持指南
  6. 06Next.js 集成指南

适用场景

  1. 01为列表到详情页的导航添加动画
  2. 02显示 Suspense 加载完成的内容
  3. 03为重排或变化的列表添加动画
  4. 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.md
metadata.json
README.md
references/
css-recipes.md
implementation.md
nextjs.md
patterns.md
troubleshooting.md

详细介绍

原文在翻译后有更新,译文可能不是最新。

React View Transitions

使用浏览器原生的 document.startViewTransition 在 UI 状态之间添加动画。使用 <ViewTransition> 声明动画什么,使用 startTransition / useDeferredValue / Suspense 触发动画时机,使用 CSS 类控制动画效果。不支持的浏览器会优雅地跳过动画。

何时添加动画

每个 <ViewTransition> 都应传达一种空间关系或连续性。如果你说不清它传达了什么,就不要添加。

按以下顺序实现此列表中的所有适用模式:

优先级模式传达的含义
1共享元素(name)“同一内容——深入查看”
2Suspense 显示内容“数据已加载”
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 添加到全局样式表。


参考文件

完整编译文档

包含所有展开的参考文件的完整指南:AGENTS.md

评论

支持 Markdown