vercel-react-view-transitions

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React View Transitions

React 视图过渡(View Transitions)

React's View Transition API lets you animate between UI states using the browser's native
document.startViewTransition
under the hood. Declare what to animate with
<ViewTransition>
, trigger when with
startTransition
/
useDeferredValue
/
Suspense
, and control how with CSS classes or the Web Animations API. Unsupported browsers skip the animation and apply the DOM change instantly.
React 的 View Transition API 底层基于浏览器原生的
document.startViewTransition
,可帮助你实现 UI 状态之间的过渡动画。通过
<ViewTransition>
声明要对什么元素做动画,通过
startTransition
/
useDeferredValue
/
Suspense
控制什么时候触发动画,通过 CSS 类或 Web Animations API 控制动画的表现形式。不支持该 API 的浏览器会跳过动画,直接应用 DOM 变更。

When to Animate (and When Not To)

何时应该使用动画(以及何时不该用)

Every
<ViewTransition>
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>
都应该能回答这个问题:这个动画能向用户传达什么样的空间关联或上下文连续性? 如果你说不清楚,就不要加这个动画。

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:
PriorityPatternWhat it communicatesExample
1Shared element (
name
)
"This is the same thing — I'm going deeper"List thumbnail morphs into detail hero
2Suspense reveal"Data loaded, here's the real content"Skeleton cross-fades into loaded page
3List identity (per-item
key
)
"Same items, new arrangement"Cards reorder during sort/filter
4State change (
enter
/
exit
)
"Something appeared or disappeared"Panel slides in on toggle
5Route 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共享元素
name
属性)
"这是同一个对象,我正在进入更深层级的页面"列表缩略图变形为详情页的头图
2Suspense 内容露出"数据加载完成,这是实际内容"骨架屏渐变为加载完成的页面
3列表元素标识(每个元素的
key
属性)
"元素不变,只是排列顺序变了"排序/筛选时卡片重排动画
4状态变更
enter
/
exit
"有元素出现或消失了"切换时侧边面板滑入
5路由切换(布局层级)"正在跳转到新页面"页面间的淡入淡出
路由层级的过渡(第5类)优先级最低,因为 URL 变化本身就已经传达了上下文切换的信息。给所有导航都加统一的淡入淡出没有任何实际意义,只是视觉噪音。优先使用有明确意图的特定动画(第1-4类),而不是通用的页面过渡效果。
经验法则: 任意时刻,组件树中只能有一个层级在执行视觉过渡。如果你的页面已经自己实现了 Suspense 内容露出或共享元素变形动画,再在布局层级加路由过渡就会导致双重动画,两个层级的效果会互相干扰。

Choosing the Right Animation Style

选择合适的动画风格

Not everything should slide. Match the animation to the spatial relationship:
ContextAnimationWhy
Detail page main content
enter="slide-up"
Reveals "deeper" content the user drilled into
Detail page outer wrapper
enter
/
exit
type map for
nav-forward
Navigating forward — horizontal direction
List / overview pagesBare
<ViewTransition>
(fade) or
default="none"
Lateral navigation — no spatial depth to communicate
Page headers / breadcrumbsBare
<ViewTransition>
(fade)
Small, fast-loading metadata — slide feels excessive
Secondary section on same page
enter="slide-up"
Second Suspense boundary streaming in after the header
Revalidation / background refresh
default="none"
Data refreshed silently — animation would be distracting
When in doubt, use a bare
<ViewTransition>
(default cross-fade) or
default="none"
. Only add directional motion (slide-up, slide-from-right) when it communicates spatial meaning.

不是所有动画都应该用滑动效果,动画类型要匹配空间关联关系:
场景动画原因
详情页主体内容
enter="slide-up"
展示用户点进的「更深层级」内容
详情页外层容器
nav-forward
配置
enter
/
exit
类型映射
向前导航 —— 水平方向的过渡
列表/概览页面
<ViewTransition>
(淡入淡出)或
default="none"
横向导航 —— 不需要传达空间深度
页面头部/面包屑
<ViewTransition>
(淡入淡出)
体积小、加载快的元数据 —— 滑动效果过于夸张
同一页面的次级模块
enter="slide-up"
头部加载完成后流式渲染的第二个 Suspense 边界
数据重校验/后台刷新
default="none"
数据静默刷新 —— 动画会分散用户注意力
拿不准的时候,就用纯
<ViewTransition>
(默认淡入淡出)或者
default="none"
。只有当动画需要传达空间含义时,再添加定向动效(上滑、从右侧滑入等)。

Availability

可用性说明

  • <ViewTransition>
    and
    addTransitionType
    require
    react@canary
    or
    react@experimental
    . They are not in stable React (including 19.x). Before implementing, verify the project uses canary — check
    package.json
    for
    "react": "canary"
    or run
    npm ls react
    . If on stable, install canary:
    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@experimental
    版本,不在稳定版 React 中提供(包括 19.x 版本)。实现前请确认项目使用的是 canary 版本:检查
    package.json
    中是否有
    "react": "canary"
    ,或者执行
    npm ls react
    查看。如果使用的是稳定版,安装 canary 版本:
    npm install react@canary react-dom@canary
  • 浏览器支持:Chromium 111+,Firefox 和 Safari 正在增加支持。API 会优雅降级,不支持的浏览器会跳过动画,直接应用 DOM 变更。

Core Concepts

核心概念

The
<ViewTransition>
Component

<ViewTransition>
组件

Wrap the elements you want to animate:
jsx
import { ViewTransition } from 'react';

<ViewTransition>
  <Component />
</ViewTransition>
React automatically assigns a unique
view-transition-name
to the nearest DOM node inside each
<ViewTransition>
, and calls
document.startViewTransition
behind the scenes. Never call
startViewTransition
yourself — React coordinates all view transitions and will interrupt external ones.
包裹你想要添加动画的元素:
jsx
import { ViewTransition } from 'react';

<ViewTransition>
  <Component />
</ViewTransition>
React 会自动给每个
<ViewTransition>
内部最近的 DOM 节点分配唯一的
view-transition-name
,并且在底层自动调用
document.startViewTransition
。永远不要自己调用
startViewTransition
,React 会协调所有视图过渡,并且会中断外部触发的过渡。

Animation Triggers

动画触发器

React decides which type of animation to run based on what changed:
TriggerWhen it fires
enterA
<ViewTransition>
is first inserted during a Transition
exitA
<ViewTransition>
is first removed during a Transition
updateDOM mutations happen inside a
<ViewTransition>
, or the boundary changes size/position due to an immediate sibling
shareA named
<ViewTransition>
unmounts and another with the same
name
mounts in the same Transition (shared element transition)
Only updates wrapped in
startTransition
,
useDeferredValue
, or
Suspense
activate
<ViewTransition>
. Regular
setState
updates immediately and does not animate.
React 会根据变更的内容决定运行哪种类型的动画:
触发器触发时机
enter过渡过程中
<ViewTransition>
首次被插入时
exit过渡过程中
<ViewTransition>
首次被移除时
update
<ViewTransition>
内部发生 DOM 变更,或者边界因为相邻兄弟元素变化导致尺寸/位置改变时
share同一个过渡过程中,一个命名的
<ViewTransition>
卸载,同时另一个同名的
<ViewTransition>
挂载时(共享元素过渡)
只有被
startTransition
useDeferredValue
Suspense
包裹的更新才会激活
<ViewTransition>
。普通的
setState
会立即更新,不会触发动画。

Critical Placement Rule

关键放置规则

<ViewTransition>
only activates enter/exit if it appears before any DOM nodes in the component tree:
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>
  );
}

只有当
<ViewTransition>
出现在组件树中所有 DOM 节点之前时,才会激活 enter/exit 动画:
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:
  • "auto"
    — use the browser default cross-fade
  • "none"
    — disable this animation type
  • "my-class-name"
    — a custom CSS class
  • An object
    { [transitionType]: value }
    for type-specific animations (see Transition Types below)
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
default
is
"none"
, all triggers are off unless explicitly listed.
每个属性控制不同的动画触发器,属性值可以是:
  • "auto"
    —— 使用浏览器默认的淡入淡出效果
  • "none"
    —— 禁用该类型的动画
  • "my-class-name"
    —— 自定义 CSS 类名
  • 格式为
    { [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:
  • ::view-transition-group(.class)
    — the container for the transition
  • ::view-transition-image-pair(.class)
    — contains old and new snapshots
  • ::view-transition-old(.class)
    — the outgoing snapshot
  • ::view-transition-new(.class)
    — the incoming snapshot

搭配类名使用视图过渡伪元素选择器:
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
lets you tag a transition with a string label so
<ViewTransition>
can pick different animations based on what caused the change. This is essential for directional navigation (forward vs. back) or distinguishing user actions (click vs. swipe vs. keyboard).
addTransitionType
允许你给过渡添加字符串标签,这样
<ViewTransition>
就可以根据变更的触发原因选择不同的动画。这对于定向导航(前进 vs 后退)或者区分用户操作类型(点击 vs 滑动 vs 键盘操作)非常重要。

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
default
key inside the object is the fallback when no type matches. If any type has the value
"none"
, the ViewTransition is disabled for that trigger.
给触发属性传对象而不是字符串,对象的键是过渡类型字符串,值是 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>
对象中的
default
键是没有匹配到任何类型时的 fallback。如果任意类型的值为
"none"
,该触发器对应的 ViewTransition 会被禁用。

Using Types with CSS
:active-view-transition-type()

结合 CSS
:active-view-transition-type()
使用过渡类型

React adds transition types as browser view transition types, enabling pure CSS scoping with
:root:active-view-transition-type(type-name)
. Caveat:
::view-transition-old(*)
/
::view-transition-new(*)
match all named elements — the wildcard can override specific class-based animations. Prefer class-based props for per-component animations; reserve
:active-view-transition-type()
for global rules.
The
types
array is also available as the second argument in event callbacks (
onEnter
,
onExit
, etc.) — see
references/patterns.md
.
React 会把过渡类型添加为浏览器视图过渡类型,支持通过
:root:active-view-transition-type(type-name)
实现纯 CSS 作用域控制。注意:
::view-transition-old(*)
/
::view-transition-new(*)
会匹配所有命名元素,通配符可能会覆盖特定类的动画。组件级动画优先使用基于类的属性,
:active-view-transition-type()
保留给全局规则使用。
types
数组也会作为事件回调(
onEnter
onExit
等)的第二个参数传入,详见
references/patterns.md

Types and Suspense: When Types Are Available

过渡类型与 Suspense:过渡类型的可用时机

When a
<Link>
with
transitionTypes
triggers navigation, the transition type is available to all
<ViewTransition>
s that enter/exit during that navigation
. An outer page-level
<ViewTransition>
with a type map sees the type and responds. Inner
<ViewTransition>
s with simple string props also enter — the type is irrelevant to them because simple strings fire regardless of type.
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
default: "none"
applies and nothing animates.
Use type maps for
<ViewTransition>
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.

当带有
transitionTypes
<Link>
触发导航时,过渡类型对导航过程中所有触发 enter/exit 的
<ViewTransition>
都可用。外层页面级带类型映射的
<ViewTransition>
可以识别到类型并做出响应。使用简单字符串属性的内层
<ViewTransition>
也会正常触发,简单字符串属性不受类型影响,无论什么类型都会生效。
后续的 Suspense 内容露出(导航完成后流式数据加载完成时)是没有类型的独立过渡。这意味着 Suspense 内容上的类型键属性不会生效:
jsx
// Suspense 内容露出时不会触发动画 —— 此时过渡类型已经消失了
<ViewTransition enter={{ "nav-forward": "slide-up", default: "none" }} default="none">
  <AsyncContent />
</ViewTransition>
当 Suspense 后续加载完成时,会触发一个新的没有类型的过渡,所以会应用
default: "none"
,没有任何动画。
直接随导航 enter/exit 的
<ViewTransition>
使用类型映射,Suspense 内容露出使用简单字符串属性。
完整示例见下文「两种模式 —— 合理隔离即可共存」中的双层模式。

Shared Element Transitions

共享元素过渡

Assign the same
name
to two
<ViewTransition>
components — one in the unmounting tree and one in the mounting tree — to animate between them as if they're the same element:
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
    <ViewTransition>
    with a given
    name
    can be mounted at a time — use globally unique names (namespace with a prefix or module constant).
  • 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>
组件分配相同的
name
—— 一个在卸载的组件树中,一个在挂载的组件树中 —— 就可以实现两个元素之间的过渡,就像它们是同一个元素一样:
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>
  );
}
共享元素过渡的规则:
  • 同一时间只能有一个对应
    name
    <ViewTransition>
    被挂载 —— 使用全局唯一的名称(加前缀命名空间或者使用模块常量)。
  • 「share」触发器优先级高于「enter」/「exit」。
  • 如果任意一侧在视口外,不会形成配对,两侧会分别作为 enter/exit 独立动画。
  • 使用共享模块中定义的常量避免名称冲突。

View Transition Events (JavaScript Animations)

视图过渡事件(JavaScript 动画)

For imperative control with
onEnter
,
onExit
,
onUpdate
,
onShare
callbacks and the
instance
object (
.old
,
.new
,
.group
,
.imagePair
,
.name
), see
references/patterns.md
. Always return a cleanup function from event handlers. Only one event fires per
<ViewTransition>
per Transition —
onShare
takes precedence over
onEnter
/
onExit
.

关于
onEnter
onExit
onUpdate
onShare
回调的命令式控制,以及
instance
对象(包含
.old
.new
.group
.imagePair
.name
属性)的使用,详见
references/patterns.md
。始终要在事件处理器中返回清理函数。每个
<ViewTransition>
每次过渡只会触发一个事件 ——
onShare
优先级高于
onEnter
/
onExit

Common Patterns

常用模式

Animate Enter/Exit of a Component

组件入场/退场动画

Conditionally render the
<ViewTransition>
itself — toggle with
startTransition
:
jsx
{show && (
  <ViewTransition enter="fade-in" exit="fade-out">
    <Panel />
  </ViewTransition>
)}
有条件地渲染
<ViewTransition>
本身 —— 用
startTransition
切换显示状态:
jsx
{show && (
  <ViewTransition enter="fade-in" exit="fade-out">
    <Panel />
  </ViewTransition>
)}

Animate List Reorder

列表重排动画

Wrap each item (not a wrapper div) in
<ViewTransition>
with a stable
key
:
jsx
{items.map(item => (
  <ViewTransition key={item.id}>
    <ItemCard item={item} />
  </ViewTransition>
))}
Triggering the reorder inside
startTransition
will smoothly animate each item to its new position. Avoid wrapper
<div>
s between the list and
<ViewTransition>
— they block the reorder animation.
How it works:
startTransition
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.
给每个元素(不是外层容器 div)包裹
<ViewTransition>
,并使用稳定的
key
jsx
{items.map(item => (
  <ViewTransition key={item.id}>
    <ItemCard item={item} />
  </ViewTransition>
))}
startTransition
内部触发重排时,每个元素会平滑地动画到新位置。避免在列表和
<ViewTransition>
之间加外层
<div>
—— 它们会阻塞重排动画。
工作原理:
startTransition
不需要异步工作也能触发动画。View Transition API 会捕获 DOM 的「变更前」快照,然后 React 应用状态更新,API 再捕获「变更后」快照。只要元素在两个快照之间位置发生了变化,动画就会运行 —— 即使是纯同步的本地状态变更比如排序也可以。

Force Re-Enter with
key

使用
key
强制重新入场

Use a
key
prop on
<ViewTransition>
to force an enter/exit animation when a value changes — even if the component itself doesn't unmount:
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
<ViewTransition>
, 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.
Caution with Suspense: If the
<ViewTransition>
wraps a
<Suspense>
, changing the key remounts the entire Suspense boundary, re-triggering the data fetch. Only use
key
on
<ViewTransition>
outside of Suspense, or accept the refetch.
<ViewTransition>
添加
key
属性,当值变化时强制触发入场/退场动画 —— 即使组件本身没有卸载:
jsx
<ViewTransition key={searchParams.toString()} enter="slide-up" exit="slide-down" default="none">
  <ResultsGrid results={results} />
</ViewTransition>
当 key 变化时,React 会卸载并重新挂载
<ViewTransition>
,触发旧实例的 exit 动画和新实例的 enter 动画。这适用于 URL 参数驱动的内容切换、标签页切换,或者任何内容标识变化但组件类型不变的状态变更场景的动画。
Suspense 注意事项: 如果
<ViewTransition>
包裹了
<Suspense>
,修改 key 会重新挂载整个 Suspense 边界,重新触发数据请求。只在 Suspense 外部的
<ViewTransition>
上使用
key
,或者接受重新请求的行为。

Animate Suspense Fallback to Content

Suspense fallback 到内容的过渡动画

The simplest approach: wrap
<Suspense>
in a single
<ViewTransition>
for a zero-config cross-fade from skeleton to content:
jsx
<ViewTransition>
  <Suspense fallback={<Skeleton />}>
    <Content />
  </Suspense>
</ViewTransition>
For directional motion, give the fallback and content separate
<ViewTransition>
s. Use
default="none"
on the content to prevent re-animation on revalidation:
jsx
<Suspense
  fallback={
    <ViewTransition exit="slide-down">
      <Skeleton />
    </ViewTransition>
  }
>
  <ViewTransition default="none" enter="slide-up">
    <AsyncContent />
  </ViewTransition>
</Suspense>
Why
exit
on the fallback and
enter
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 (
enter
delays by the
exit
duration) ensures the skeleton leaves before new content arrives.
最简单的实现方式:给
<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 加
exit
,给内容加
enter
当 Suspense 加载完成时,同一个过渡中会同时发生两件事:fallback 卸载(触发 exit)和内容挂载(触发 enter)。Fallback 下滑淡出的同时内容上滑淡入,实现平滑的交接。错开的 CSS 时序(
enter
延迟
exit
的时长)可以确保骨架屏在新内容到达前完全离开。

Opt Out of Nested Animations

退出嵌套动画

Wrap children in
<ViewTransition update="none">
to prevent them from animating when a parent changes:
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
<Activity>
, exclude elements with
useOptimistic
), see
references/patterns.md
.

给子元素包裹
<ViewTransition update="none">
,避免父元素变化时子元素也跟着触发动画:
jsx
<ViewTransition>
  <div className={theme}>
    <ViewTransition update="none">
      {children}
    </ViewTransition>
  </div>
</ViewTransition>
更多模式(隔离持久/悬浮元素、可复用的动画折叠组件、使用
<Activity>
保留状态、使用
useOptimistic
排除元素)详见
references/patterns.md

How Multiple
<ViewTransition>
s Interact

多个
<ViewTransition>
的交互逻辑

When a transition fires, every
<ViewTransition>
in the tree that matches the trigger participates simultaneously. Each gets its own
view-transition-name
, and the browser animates all of them inside a single
document.startViewTransition
call. They run in parallel, not sequentially.
This means multiple
<ViewTransition>
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
document.startViewTransition
produces competing animations. But
<ViewTransition>
s that fire in different transitions (e.g., navigation vs. a later Suspense resolve) don't compete — they animate at different moments.
当过渡触发时,组件树中所有匹配触发器的
<ViewTransition>
会同时参与。每个都会获得自己的
view-transition-name
,浏览器会在同一个
document.startViewTransition
调用中给所有元素添加动画。它们是并行运行的,不是串行的。
这意味着在同一个过渡中触发的多个
<ViewTransition>
会同时动画。布局级的淡入淡出 + 页面级的上滑动画 + 元素重排动画都在同一个
document.startViewTransition
中运行,会导致动画互相冲突。但在不同过渡中触发的
<ViewTransition>
(比如导航 vs 后续的 Suspense 加载完成)不会冲突 —— 它们会在不同的时间点动画。

Use
default="none"
Liberally

多使用
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
enter
/
exit
, the
ViewTransitionClassPerType
type requires a
default
key. Always include
default: 'none'
(or
'auto'
) in the object — omitting it causes a type error even if the component-level
default
prop is set.
Without
default="none"
, a
<ViewTransition>
with
default="auto"
(the implicit default) fires the browser's cross-fade on every transition — including ones triggered by child Suspense boundaries,
useDeferredValue
updates, or
startTransition
calls within the page.
Next.js revalidation: This is especially important in Next.js — when
revalidateTag()
fires (from a Server Action, webhook, or polling), the page re-renders. Without
default="none"
, every
<ViewTransition>
in the tree re-animates: content slides up again, things flash. Always use
default="none"
on content
<ViewTransition>
s and only enable specific triggers (
enter
,
exit
) explicitly.
给只需要针对特定类型触发的 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 注意:
enter
/
exit
传对象时,
ViewTransitionClassPerType
类型要求必须有
default
键。始终要在对象中包含
default: 'none'
(或
'auto'
)—— 即使组件级的
default
属性已经设置了,省略它也会导致类型错误。
如果不设置
default="none"
,默认
default="auto"
<ViewTransition>
会在每一次过渡中都触发浏览器的淡入淡出动画 —— 包括子 Suspense 边界触发的更新、
useDeferredValue
更新,或者页面内部的
startTransition
调用。
Next.js 重校验场景: 这在 Next.js 中尤其重要 —— 当
revalidateTag()
触发时(来自服务端动作、webhook 或轮询),页面会重新渲染。如果没有
default="none"
,组件树中所有的
<ViewTransition>
都会重新动画:内容再次上滑、元素闪烁。始终给内容
<ViewTransition>
添加
default="none"
,只显式启用特定的触发器(
enter
exit
)。

Two Patterns — Can Coexist with Proper Isolation

两种模式 —— 合理隔离即可共存

There are two distinct view transition patterns:
Pattern A — Directional page slides (e.g., left/right navigation):
  • Uses
    transitionTypes
    on
    <Link>
    or
    addTransitionType
    to tag navigation direction
  • An outer
    <ViewTransition>
    on the page maps types to slide classes with
    default="none"
  • Fires during the navigation transition (when the type is present)
Pattern B — Suspense content reveals (e.g., streaming data):
  • No
    transitionTypes
    needed
  • Simple
    enter="slide-up"
    /
    exit="slide-down"
    on
    <ViewTransition>
    s around Suspense boundaries
  • default="none"
    prevents re-animation on revalidation
  • 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).
default="none"
on both layers prevents cross-interference — the nav VT ignores Suspense resolves, and the Suspense VT ignores navigations:
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
enter
with
exit
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.
When they DO conflict: If both layers use
default="auto"
, or if a layout-level
<ViewTransition>
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.
Place the outer directional
<ViewTransition>
in each page component — not in a layout (layouts persist and don't trigger enter/exit). Per-page wrappers are the cleanest approach.
Shared element transitions (
name
prop) work alongside either pattern because the
share
trigger takes precedence over
enter
/
exit
.

有两种不同的视图过渡模式:
模式 A —— 定向页面滑动(比如左右导航):
  • <Link>
    上使用
    transitionTypes
    或者
    addTransitionType
    标记导航方向
  • 页面外层的
    <ViewTransition>
    把类型映射为滑动类,设置
    default="none"
  • 在导航过渡期间触发(此时过渡类型存在)
模式 B —— Suspense 内容露出(比如流式数据加载):
  • 不需要
    transitionTypes
  • 给 Suspense 边界周围的
    <ViewTransition>
    设置简单的
    enter="slide-up"
    /
    exit="slide-down"
  • default="none"
    避免重校验时重复动画
  • 数据加载完成后触发(没有类型的独立过渡)
当它们在不同时间触发时可以共存。 导航滑动在导航过渡期间触发(有类型);Suspense 内容露出在后续数据流式加载完成时触发(无类型)。两层都设置
default="none"
避免交叉干扰 —— 导航 VT 忽略 Suspense 加载完成事件,Suspense VT 忽略导航事件:
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>
定向过渡中始终要配对
enter
exit
如果没有 exit 动画,旧页面会瞬间消失,新页面在滚动位置0滑入 —— 会有突兀的跳转。退出滑动可以在过渡快照中掩盖滚动变化,因为旧内容会同时动画退出。
什么时候会冲突: 如果两层都使用
default="auto"
,或者布局级的
<ViewTransition>
和页面级的上滑动画在同一个过渡中触发淡入淡出,它们会同时动画,互相干扰。冲突是因为同时刻的动画,而不是因为在同一个页面使用两种模式。
把外层定向
<ViewTransition>
放在每个页面组件中 —— 不要放在布局里(布局是持久的,不会触发 enter/exit)。每个页面单独包裹是最简洁的实现方式。
共享元素过渡(
name
属性)可以和任意模式共存,因为
share
触发器优先级高于
enter
/
exit

Next.js Integration

Next.js 集成

Next.js supports React View Transitions.
<ViewTransition>
works out of the box for
startTransition
- and
Suspense
-triggered updates — no config needed.
To also animate
<Link>
navigations, enable the experimental flag in
next.config.js
(or
next.config.ts
):
js
const nextConfig = {
  experimental: {
    viewTransition: true,
  },
};
module.exports = nextConfig;
What this flag does: It wraps every
<Link>
navigation in
document.startViewTransition
, so all mounted
<ViewTransition>
components participate in every link click. Without this flag, only
startTransition
/
Suspense
-triggered transitions animate. This makes the composition rules in "How Multiple
<ViewTransition>
s Interact" especially important: use
default="none"
on layout-level
<ViewTransition>
s to avoid competing animations.
For a detailed guide including App Router patterns and Server Component considerations, see
references/nextjs.md
.
Key points:
  • The
    <ViewTransition>
    component is imported from
    react
    directly — no Next.js-specific import.
  • Works with the App Router and
    startTransition
    +
    router.push()
    for programmatic navigation.
Next.js 支持 React View Transitions。
<ViewTransition>
开箱即支持
startTransition
Suspense
触发的更新 —— 不需要额外配置。
如果要给
<Link>
导航也添加动画,需要在
next.config.js
(或
next.config.ts
)中启用实验性 flag:
js
const nextConfig = {
  experimental: {
    viewTransition: true,
  },
};
module.exports = nextConfig;
这个 flag 的作用: 它会把每个
<Link>
导航都包裹在
document.startViewTransition
中,所以所有挂载的
<ViewTransition>
组件都会参与每次链接点击的过渡。如果没有这个 flag,只有
startTransition
/
Suspense
触发的过渡才会动画。这让「多个
<ViewTransition>
的交互逻辑」部分的组合规则尤其重要:给布局级
<ViewTransition>
使用
default="none"
避免动画冲突。
包含 App Router 模式和服务端组件注意事项的详细指南见
references/nextjs.md
关键点:
  • <ViewTransition>
    组件直接从
    react
    导入 —— 不需要 Next.js 特定的导入。
  • 支持 App Router 模式,以及编程式导航的
    startTransition
    +
    router.push()
    用法。

The
transitionTypes
prop on
next/link

next/link
transitionTypes
属性

next/link
supports a native
transitionTypes
prop — pass an array of strings directly, no
'use client'
or wrapper component needed:
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.md
.

next/link
原生支持
transitionTypes
属性 —— 直接传字符串数组即可,不需要
'use client'
或者封装组件:
tsx
<Link href="/products/1" transitionTypes={['transition-to-detail']}>查看商品</Link>
包含共享元素过渡和定向动画的完整示例见
references/nextjs.md

Accessibility

可访问性

Always respect
prefers-reduced-motion
. React does not disable animations automatically for this preference. Add this to your global CSS:
css
@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.

始终要尊重
prefers-reduced-motion
设置。React 不会自动针对该偏好禁用动画。在全局 CSS 中添加以下代码:
css
@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

参考文件

  • references/patterns.md
    — Real-world patterns (searchable grids, expand/collapse, type-safe helpers), animation timing, view transition events (JavaScript Animations API), and troubleshooting.
  • references/css-recipes.md
    — Ready-to-use CSS animation recipes (slide, fade, scale, directional nav, and combined patterns).
  • references/nextjs.md
    — Detailed Next.js integration guide with App Router patterns and Server Component considerations.
  • references/patterns.md
    —— 真实场景的模式(可搜索网格、展开/折叠、类型安全的辅助工具)、动画时序、视图过渡事件(JavaScript Animations API)以及问题排查。
  • references/css-recipes.md
    —— 开箱即用的 CSS 动画模板(滑动、淡入淡出、缩放、定向导航以及组合模式)。
  • references/nextjs.md
    —— 详细的 Next.js 集成指南,包含 App Router 模式和服务端组件注意事项。",