Files
skillhub-043-vercel-react-v…/SKILL.md
T
2026-07-13 21:35:52 +08:00

12 KiB
Raw Blame History

name, description, license, metadata
name description license metadata
vercel-react-view-transitions 使用 React 视图过渡 API`<ViewTransition>` 组件、`addTransitionType` 和 CSS 视图过渡伪元素)实现流畅、原生感动画的指南。当用户希望添加页面过渡、动画化路由变化、创建共享元素动画、动画化组件进入/退出、动画化列表重排、实现方向性(前进/后退)导航动画,或在 Next.js 中集成视图过渡时,应使用此技能。当用户提及视图过渡、`startViewTransition`、`ViewTransition`、过渡类型,或询问如何在 React 中不使用第三方动画库来实现 UI 状态间的动画时,也应使用此技能。 MIT
author version
vercel 1.0.0

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@canaryViewTransition 不在稳定版 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 挂载

只有 startTransitionuseDeferredValueSuspense 能激活 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 类。适用于 enterexit 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>

enterexit 不必对称。例如,淡入但按方向滑出:

<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" 防止交叉干扰。始终将 enterexit 配对。将方向性 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