<ViewTransition> - This feature is available in the latest Canary version of React

Canary

<ViewTransition /> API 当前仅在 React 的 Canary 和 Experimental 版本中可用。

在这里了解更多关于 React 发布通道的信息。

<ViewTransition> 让你可以使用过渡和 Suspense 为组件树添加动画。

import {ViewTransition} from 'react';

<ViewTransition>
<div>...</div>
</ViewTransition>

参考

🌐 Reference

<ViewTransition>

将组件树封装在 <ViewTransition> 中以对其进行动画处理:

🌐 Wrap a component tree in <ViewTransition> to animate it:

<ViewTransition>
<Page />
</ViewTransition>

查看更多示例。

深入研究

<ViewTransition> 是如何工作的?

🌐 How does <ViewTransition> work?

Under the hood, React applies view-transition-name to inline styles of the nearest DOM node nested inside the <ViewTransition> component. If there are multiple sibling DOM nodes like <ViewTransition><div /><div /></ViewTransition> then React adds a suffix to the name to make each unique but conceptually they’re part of the same one. React doesn’t apply these eagerly but only at the time that boundary should participate in an animation.

React 会自动在后台调用 startViewTransition,所以你自己绝不应该这么做。实际上,如果页面上有其他东西正在运行 ViewTransition,React 会中断它。因此建议你使用 React 本身来协调这些。如果你过去有其他触发 ViewTransition 的方式,我们建议你迁移到内置方式。

🌐 React automatically calls startViewTransition itself behind the scenes so you should never do that yourself. In fact, if you have something else on the page running a ViewTransition React will interrupt it. So it’s recommended that you use React itself to coordinate these. If you had other ways to trigger ViewTransitions in the past, we recommend that you migrate to the built-in way.

如果已经有其他 React ViewTransitions 正在运行,那么 React 会等它们完成后再开始下一个。然而,重要的是,如果第一个动画运行期间有多个更新发生,这些更新都会被合并为一个。如果你启动了 A->B。然后在此期间你又收到了一个更新,要跳转到 C 然后 D。当第一个 A->B 动画结束后,下一个动画将从 B->D 执行。

🌐 If there are other React ViewTransitions already running then React will wait for them to finish before starting the next one. However, importantly if there are multiple updates happening while the first one is running, those will all be batched into one. If you start A->B. Then in the meantime you get an update to go to C and then D. When the first A->B animation finishes the next one will animate from B->D.

getSnapshotBeforeUpdate 生命周期将在 startViewTransition 之前被调用,并且一些 view-transition-name 会同时更新。

🌐 The getSnapshotBeforeUpdate lifecycle will be called before startViewTransition and some view-transition-name will update at the same time.

然后 React 调用 startViewTransition。在 updateCallback 内,React 将会:

🌐 Then React calls startViewTransition. Inside the updateCallback, React will:

  • 将其变更应用到 DOM 并调用 useInsertionEffect
  • 等待字体加载。
  • 参见 componentDidMountcomponentDidUpdateuseLayoutEffect 及参考文献。
  • 等待任何待处理的导航完成。
  • 然后 React 将测量布局的任何变化,以确定哪些边界需要动画。

startViewTransition 的 ready Promise 被解决后,React 将会恢复 view-transition-name。然后 React 会调用 onEnteronExitonUpdateonShare 回调,以允许对动画进行手动编程控制。这会在内置默认动画已经计算完成之后发生。

🌐 After the ready Promise of the startViewTransition is resolved, React will then revert the view-transition-name. Then React will invoke the onEnter, onExit, onUpdate and onShare callbacks to allow for manual programmatic control over the animations. This will be after the built-in default ones have already been computed.

如果一个 flushSync 恰好出现在这个序列的中间,那么 React 将跳过 Transition,因为它依赖于能够同步完成。

🌐 If a flushSync happens to get in the middle of this sequence, then React will skip the Transition since it relies on being able to complete synchronously.

startViewTransition 的完成 Promise 被解决后,React 将会调用 useEffect。这可以防止它们干扰动画的性能。然而,这并不能保证,因为如果动画运行过程中发生另一个 setState,它仍然必须提前调用 useEffect 以保持顺序保证。

🌐 After the finished Promise of the startViewTransition is resolved, React will then invoke useEffect. This prevents those from interfering with the performance of the animation. However, this is not a guarantee because if another setState happens while the animation is running it’ll still have to invoke the useEffect earlier to preserve the sequential guarantees.

属性

🌐 Props

  • 可选 name:一个字符串或对象。用于共享元素过渡的视图过渡名称。如果未提供,React 将为每个视图过渡使用唯一名称,以防止意外动画。
  • 视图过渡类 属性。
  • 查看过渡事件 属性。

注意事项

🌐 Caveats

  • 仅将 name 用于 共享元素过渡。对于所有其他动画,React 会自动生成一个唯一名称以防止意外动画。
  • 默认情况下,setState 会立即更新,并且不会激活 <ViewTransition>,只有封装在 Transition<Suspense>useDeferredValue 中的更新才会激活 ViewTransition。
  • <ViewTransition> 创建一个可以移动、缩放和交叉淡入淡出的图片。与你可能在 React Native 或 Motion 中见过的布局动画不同,这意味着其中的每个单独元素并不会动画化其位置。这可以带来更好的性能和更连续、平滑的动画体验,相比之下动画化每个单独的部分要流畅得多。然而,这也可能导致本应独立移动的内容失去连续性。因此,你可能需要手动添加更多的 <ViewTransition> 边界。
  • 目前,<ViewTransition> 仅在 DOM 中可用。我们正在努力添加对 React Native 和其他平台的支持。

动画触发器

🌐 Animation triggers

React 会自动决定要触发的视图过渡动画类型:

🌐 React automatically decides the type of View Transition animation to trigger:

  • enter:如果 ViewTransition 是在此转换中插入的第一个组件,则此功能将激活。
  • exit:如果在这个过渡中 ViewTransition 是第一个被删除的组件,那么这将被激活。
  • update:如果一个 ViewTransition 内部存在 React 正在进行的任何 DOM 变化(例如 prop 发生变化),或者 ViewTransition 边界本身由于紧邻的元素而改变大小或位置。如果存在嵌套的 ViewTransition,则该变化适用于它们而不是父元素。
  • share:如果一个名为 ViewTransition 的元素位于被删除的子树中,而另一个同名为 ViewTransition 的元素是同一过渡中插入的子树的一部分,它们会形成共享元素过渡,并且过渡会从被删除的元素动画到插入的元素。

默认情况下,<ViewTransition> 会以平滑的交叉淡入淡出动画(浏览器的默认视图过渡)进行动画处理。

🌐 By default, <ViewTransition> animates with a smooth cross-fade (the browser default view transition).

你可以通过为每种触发类型向 <ViewTransition> 组件提供一个 视图过渡类 来自定义动画(参见 样式化视图过渡),或者通过使用 ViewTransition 事件 使用 Web 动画 API 用 JavaScript 控制动画。

🌐 You can customize the animation by providing a View Transition Class to the <ViewTransition> component for each kind of trigger (see Styling View Transitions), or by using ViewTransition Events to control the animation with JavaScript using the Web Animations API.

注意

始终检查 prefers-reduced-motion

🌐 Always check prefers-reduced-motion

许多用户可能更喜欢页面上没有动画。React 并不会自动为这种情况禁用动画。

🌐 Many users may prefer not having animations on the page. React doesn’t automatically disable animations for this case.

我们建议始终使用 @media (prefers-reduced-motion) 媒体查询,根据用户偏好禁用动画或降低动画效果。

🌐 We recommend always using the @media (prefers-reduced-motion) media query to disable animations or tone them down based on user preference.

未来,CSS 库可能会将其内置到其预设中。

🌐 In the future, CSS libraries may have this built-in to their presets.

视图过渡类

🌐 View Transition Class

<ViewTransition> 提供属性来定义触发哪些动画:

<ViewTransition
default="none"
enter="slide-up"
exit="slide-down"
/>

属性

🌐 Props

  • 可选 enter"auto""none"、字符串或对象。
  • 可选 exit"auto""none"、字符串或对象。
  • 可选 update"auto""none"、字符串或对象。
  • 可选 share"auto""none"、字符串或对象。
  • 可选 default"auto""none"、字符串,或一个对象。

注意事项

🌐 Caveats

  • 如果 default"none",则除非明确列出,否则所有其他触发器都会被关闭。

🌐 Values

视图过渡类的值可以是:

🌐 View Transition class values can be:

  • auto:默认。使用浏览器的默认动画。
  • none:为此类型禁用动画。
  • <classname>:用于自定义视图过渡的自定义 CSS 类名。

对象的值可以是一个带有字符串键的对象,其值为 autonone 或自定义 className:

🌐 Object values can be an object with string keys and a value of auto, none or a custom className:

  • {[type]: value}:如果动画与 Transition Type 匹配,则应用 value
  • {default: value}:如果没有匹配的 Transition Type 要应用的默认值。

例如,你可以将 ViewTransition 定义为:

🌐 For example, you can define a ViewTransition as:

<ViewTransition
/* turn off any animation not defined below */
default="none"
enter={{
/* apply slide-in for Transition Type `forward` */
"forward": 'slide-in',
/* otherwise use the browser default animation */
"default": 'auto'
}}
/* use the browser default for exit animations*/
exit="auto"
/* apply a custom `cross-fade` class for updates */
update="cross-fade"
>

请参阅 Styling View Transitions 了解如何为自定义动画定义 CSS 类。

🌐 See Styling View Transitions for how to define CSS classes for custom animations.


视图过渡事件

🌐 View Transition Event

视图过渡事件允许你使用 JavaScript 和 Web 动画 API 控制动画:

🌐 View Transition Events allow you to control the animation with JavaScript using the Web Animations API:

<ViewTransition
onEnter={instance => }
onExit={instance => }
/>

属性

🌐 Props

  • 可选 onEnter:在触发“进入”动画时调用。
  • 可选 onExit:在触发“退出”动画时调用。
  • 可选 onShare:在触发“分享”动画时调用。
  • 可选 onUpdate:在触发“更新”动画时调用。

注意事项

🌐 Caveats

  • 每次转换中每个 <ViewTransition> 只会触发一个事件。onShare 优先于 onEnteronExit
  • 每个事件都应返回一个清理函数。当视图切换完成时,会调用清理函数,从而允许你取消或清理任何动画。

参数

🌐 Arguments

每个事件接收两个参数:

🌐 Each event receives two arguments:

  • instance:一个视图过渡实例,提供对视图过渡pseudo-elements的访问
    • old::view-transition-old 伪元素。
    • new::view-transition-new 伪元素。
    • name:此边界的 view-transition-name 字符串。
    • group::view-transition-group 伪元素。
    • imagePair::view-transition-image-pair 伪元素。
  • types:动画中包含的 过渡类型Array<string>。如果未指定类型,则为一个空数组。

例如,你可以定义一个 onEnter 事件来使用 JavaScript 驱动动画:

🌐 For example, you can define a onEnter event that drives the animation using JavaScript:

<ViewTransition
onEnter={(instance, types) => {
const anim = instance.new.animate([{opacity: 0}, {opacity: 1}], {
duration: 500,
});
return () => anim.cancel();
}}>
<div>...</div>
</ViewTransition>

查看更多示例,请参见 使用 JavaScript 动画

🌐 See Animating with JavaScript for more examples.


视图过渡样式

🌐 Styling View Transitions

注意

在网络上许多早期的视图过渡示例中,你会看到使用 view-transition-name,然后使用 ::view-transition-...(my-name) 选择器对其进行样式设置。我们不推荐那种样式设置方式。相反,我们通常建议使用视图过渡类。

🌐 In many early examples of View Transitions around the web, you’ll have seen using a view-transition-name and then style it using ::view-transition-...(my-name) selectors. We don’t recommend that for styling. Instead, we normally recommend using a View Transition Class instead.

要为 <ViewTransition> 自定义动画,你可以向其中一个激活属性提供一个视图过渡类。视图过渡类是一个 CSS 类名,当视图过渡激活时,React 会将其应用到子元素上。

🌐 To customize the animation for a <ViewTransition> you can provide a View Transition Class to one of the activation props. The View Transition Class is a CSS class name that React applies to the child elements when the ViewTransition activates.

例如,要自定义“进入”动画,请向 enter 属性提供一个类名:

🌐 For example, to customize an “enter” animation, provide a class name to the enter prop:

<ViewTransition enter="slide-in">

<ViewTransition> 激活“进入”动画时,React 会添加类名 slide-in。然后,你可以使用 视图过渡伪选择器 引用此类来构建可重用的动画:

🌐 When the <ViewTransition> activates an “enter” animation, React will add the class name slide-in. Then you can refer to this class using view transition pseudo selectors to build reusable animations:

::view-transition-group(.slide-in) {
}
::view-transition-old(.slide-in) {
}
::view-transition-new(.slide-in) {
}

未来,CSS 库可能会使用视图过渡类添加内置动画,以使其更易于使用。

🌐 In the future, CSS libraries may add built-in animations using View Transition Classes to make this easier to use.


用法

🌐 Usage

为元素添加进入/退出动画

🌐 Animating an element on enter/exit

当组件在过渡中添加或移除 <ViewTransition> 时,会触发进入/退出过渡:

🌐 Enter/Exit Transitions trigger when a <ViewTransition> is added or removed by a component in a transition:

function Child() {
return (
<ViewTransition enter="auto" exit="auto" default="none">
<div>Hi</div>
</ViewTransition>
);
}

function Parent() {
const [show, setShow] = useState();
if (show) {
return <Child />;
}
return null;
}

当调用 setShow 时,show 切换到 true,并且渲染 Child 组件。当在 startTransition 内调用 setShow,且 Child 在其他任何 DOM 节点之前渲染 ViewTransition 时,将触发 enter 动画。

🌐 When setShow is called, show switches to true and the Child component is rendered. When setShow is called inside startTransition, and Child renders a ViewTransition before any other DOM nodes, an enter animation is triggered.

show 切换回 false 时,会触发一个 exit 动画。

🌐 When show switches back to false, an exit animation is triggered.

import {ViewTransition, useState, startTransition} from 'react';
import {Video} from './Video';
import videos from './data';

function Item() {
  return (
    <ViewTransition enter="auto" exit="auto" default="none">
      <Video video={videos[0]} />
    </ViewTransition>
  );
}

export default function Component() {
  const [showItem, setShowItem] = useState(false);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setShowItem((prev) => !prev);
          });
        }}>
        {showItem ? '➖' : '➕'}
      </button>

      {showItem ? <Item /> : null}
    </>
  );
}

易犯错误

只有顶层 ViewTransitions 在退出/进入时会动画化

🌐 Only top-level ViewTransitions animate on exit/enter

<ViewTransition> 只有在被放置在任何 DOM 节点 之前 才会激活退出/进入。

如果 <div><ViewTransition> 上方,则不会触发退出/进入动画:

🌐 If there’s a <div> above <ViewTransition>, no exit/enter animations trigger:

function Item() {
return (
<div>
<ViewTransition enter="auto" exit="auto" default="none">
<Video video={videos[0]} />
</ViewTransition>
</div>
);
}

这个约束可以防止动画过多或过少时出现微妙的错误。

🌐 This constraint prevents subtle bugs where too much or too little animates.


使用 Activity 动画进入/退出

🌐 Animating enter/exit with Activity

如果你想在保持组件状态的同时让其进出动画,或者为动画预渲染内容,你可以使用 <Activity>。当一个 <ViewTransition><Activity> 内变为可见时,enter 动画会激活。当它变为隐藏时,exit 动画会激活:

🌐 If you want to animate a component in and out while preserving its state, or pre-rendering content for an animation, you can use <Activity>. When a <ViewTransition> inside an <Activity> becomes visible, the enter animation activates. When it becomes hidden, the exit animation activates:

<Activity mode={isVisible ? 'visible' : 'hidden'}>
<ViewTransition enter="auto" exit="auto">
<Counter />
</ViewTransition>
</Activity>

在此示例中,Counter 有一个具有内部状态的计数器。尝试增加计数器的值,隐藏它,然后再次显示它。计数器的值在侧边栏进出动画时会被保留:

🌐 In this example, Counter has a counter with internal state. Try incrementing the counter, hiding it, then showing it again. The counter’s value is preserved while the sidebar animates in and out:

import { Activity, ViewTransition, useState, startTransition } from 'react';

export default function App() {
  const [show, setShow] = useState(true);
  return (
    <div className="layout">
      <Toggle show={show} setShow={setShow} />
      <Activity mode={show ? 'visible' : 'hidden'}>
        <ViewTransition enter="auto" exit="auto" default="none">
          <Counter />
        </ViewTransition>
      </Activity>
    </div>
  );
}
function Toggle({show, setShow}) {
  return (
    <button
      className="toggle"
      onClick={() => {
        startTransition(() => {
          setShow(s => !s);
        });
      }}>
      {show ? 'Hide' : 'Show'}
    </button>
  )
}
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div className="counter">
      <h2>Counter</h2>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

没有 <Activity>,计数器每次侧边栏重新出现时都会重置为 0

🌐 Without <Activity>, the counter would reset to 0 every time the sidebar reappears.


为共享元素添加动画

🌐 Animating a shared element

通常,我们不建议给 <ViewTransition> 分配一个名称,而是让 React 自动分配名称。你可能想要分配名称的原因是,当一个树卸载而另一个树同时挂载时,可以在完全不同的组件之间进行动画,以保持连续性。

🌐 Normally, we don’t recommend assigning a name to a <ViewTransition> and instead let React assign it an automatic name. The reason you might want to assign a name is to animate between completely different components when one tree unmounts and another tree mounts at the same time, to preserve continuity.

<ViewTransition name={UNIQUE_NAME}>
<Child />
</ViewTransition>

当一棵树卸载而另一棵树挂载时,如果存在一对在卸载树和挂载树中具有相同名称的节点,它们会在两者上触发“共享”动画。动画从卸载的一侧移动到挂载的一侧。

🌐 When one tree unmounts and another mounts, if there’s a pair where the same name exists in the unmounting tree and the mounting tree, they trigger the “share” animation on both. It animates from the unmounting side to the mounting side.

与退出/进入动画不同,这可以深入到已删除/已挂载的树中。如果 <ViewTransition> 也有资格进行退出/进入动画,那么“共享”动画优先。

🌐 Unlike an exit/enter animation this can be deeply inside the deleted/mounted tree. If a <ViewTransition> would also be eligible for exit/enter, then the “share” animation takes precedence.

如果 Transition 首先卸载一侧,然后导致显示 <Suspense> 回退,然后最终挂载新名称,则不会发生共享元素过渡。

🌐 If Transition first unmounts one side and then leads to a <Suspense> fallback being shown before eventually the new name being mounted, then no shared element transition happens.

import {ViewTransition, useState, startTransition} from 'react';
import {Video, Thumbnail, FullscreenVideo} from './Video';
import videos from './data';

export default function Component() {
  const [fullscreen, setFullscreen] = useState(false);
  if (fullscreen) {
    return (
      <FullscreenVideo
        video={videos[0]}
        onExit={() => startTransition(() => setFullscreen(false))}
      />
    );
  }
  return (
    <Video
      video={videos[0]}
      onClick={() => startTransition(() => setFullscreen(true))}
    />
  );
}

注意

如果一对的已安装或未安装一侧在视口之外,则不会形成配对。这确保了在滚动时它不会飞入或飞出视口。相反,它会被视为独立的常规进入/退出。

🌐 If either the mounted or unmounted side of a pair is outside the viewport, then no pair is formed. This ensures that it doesn’t fly in or out of the viewport when something is scrolled. Instead it’s treated as a regular enter/exit by itself.

如果同一个组件实例改变位置,这种情况不会发生,这会触发“更新”。无论一个位置是否在视口之外,这些都会进行动画。

🌐 This does not happen if the same Component instance changes position, which triggers an “update”. Those animate regardless of whether one position is outside the viewport.

已有一个已知的情况:如果一个深度嵌套的未挂载 <ViewTransition> 位于视口内,但已挂载的一侧不在视口内,那么即使它深度嵌套,未挂载的一侧也会作为自身的“退出”动画进行动画,而不是作为父动画的一部分。

🌐 There is a known case where if a deeply nested unmounted <ViewTransition> is inside the viewport but the mounted side is not within the viewport, then the unmounted side animates as its own “exit” animation even if it’s deeply nested instead of as part of the parent animation.

易犯错误

在整个应用中,同时只挂载一个同名的东西是很重要的。因此使用唯一的命名空间来命名以避免冲突非常重要。为了确保你可以做到这一点,你可能需要在一个单独的模块中添加一个常量,然后导入它。

🌐 It’s important that there’s only one thing with the same name mounted at a time in the entire app. Therefore it’s important to use unique namespaces for the name to avoid conflicts. To ensure you can do this you might want to add a constant in a separate module that you import.

export const MY_NAME = "my-globally-unique-name";
import {MY_NAME} from './shared-name';
...
<ViewTransition name={MY_NAME}>

为列表中元素的重新排序添加动画

🌐 Animating reorder of items in a list

items.map((item) => <Component key={item.id} item={item} />);

在重新排序列表时,如果不更新内容,每个列表中的 <ViewTransition>(如果它们在 DOM 节点之外)都会触发“更新”动画。类似于进入/退出动画。

🌐 When reordering a list, without updating the content, the “update” animation triggers on each <ViewTransition> in the list if they’re outside a DOM node. Similar to enter/exit animations.

这意味着这将触发此 <ViewTransition> 上的动画:

🌐 This means that this will trigger the animation on this <ViewTransition>:

function Component() {
return (
<ViewTransition>
<div>...</div>
</ViewTransition>
);
}
import {ViewTransition, useState, startTransition} from 'react';
import {Video} from './Video';
import videos from './data';

export default function Component() {
  const [orderedVideos, setOrderedVideos] = useState(videos);
  const reorder = () => {
    startTransition(() => {
      setOrderedVideos((prev) => {
        return [...prev.sort(() => Math.random() - 0.5)];
      });
    });
  };
  return (
    <>
      <button onClick={reorder}>🎲</button>
      <div className="listContainer">
        {orderedVideos.map((video, i) => {
          return (
            <ViewTransition key={video.title}>
              <Video video={video} />
            </ViewTransition>
          );
        })}
      </div>
    </>
  );
}

然而,这不会为每个单独的项目设置动画:

🌐 However, this wouldn’t animate each individual item:

function Component() {
return (
<div>
<ViewTransition>...</ViewTransition>
</div>
);
}

相反,任何父级 <ViewTransition> 都会交叉淡入淡出。如果没有父级 <ViewTransition>,那么在这种情况下就没有动画。

🌐 Instead, any parent <ViewTransition> would cross-fade. If there is no parent <ViewTransition> then there’s no animation in that case.

import {ViewTransition, useState, startTransition} from 'react';
import {Video} from './Video';
import videos from './data';

export default function Component() {
  const [orderedVideos, setOrderedVideos] = useState(videos);
  const reorder = () => {
    startTransition(() => {
      setOrderedVideos((prev) => {
        return [...prev.sort(() => Math.random() - 0.5)];
      });
    });
  };
  return (
    <>
      <button onClick={reorder}>🎲</button>
      <ViewTransition>
        <div className="listContainer">
          {orderedVideos.map((video, i) => {
            return <Video video={video} key={video.title} />;
          })}
        </div>
      </ViewTransition>
    </>
  );
}

这意味着你可能希望避免在列表中使用封装元素,因为你想允许组件控制其自身的重新排序动画:

🌐 This means you might want to avoid wrapper elements in lists where you want to allow the Component to control its own reorder animation:

items.map(item => <div><Component key={item.id} item={item} /></div>)

如果其中一项更新以调整大小,上述规则也适用,这会导致其兄弟项调整大小,它也会对其兄弟 <ViewTransition> 进行动画,但前提是它们是直接兄弟。

🌐 The above rule also applies if one of the items updates to resize, which then causes the siblings to resize, it’ll also animate its sibling <ViewTransition> but only if they’re immediate siblings.

这意味着在更新期间,如果导致大量重新布局,它不会对页面上的每个 <ViewTransition> 单独进行动画。这会导致大量干扰性的动画,从而分散对实际变化的注意力。因此,React 对于何时触发单个动画更加保守。

🌐 This means that during an update, which causes a lot of re-layout, it doesn’t individually animate every <ViewTransition> on the page. That would lead to a lot of noisy animations which distracts from the actual change. Therefore React is more conservative about when an individual animation triggers.

易犯错误

在重新排序列表时,正确使用键以保持身份非常重要。看起来你可能会使用“name”、共享元素过渡来动画化重新排序,但如果一侧在视口之外,这将不会触发。要动画化重新排序,你通常希望显示它移到视口之外的位置。

🌐 It’s important to properly use keys to preserve identity when reordering lists. It might seem like you could use “name”, shared element transitions, to animate reorders but that would not trigger if one side was outside the viewport. To animate a reorder you often want to show that it went to a position outside the viewport.


基于 Suspense 内容添加动画

🌐 Animating from Suspense content

像任何过渡一样,React 会在运行动画之前等待数据和新的 CSS(<link rel="stylesheet" precedence="...">)。除此之外,ViewTransitions 还会在开始动画之前等待最多 500 毫秒以加载新字体,以避免它们在后续出现闪烁。出于同样的原因,封装在 ViewTransition 中的图片会等待图片加载完成。

🌐 Like any Transition, React waits for data and new CSS (<link rel="stylesheet" precedence="...">) before running the animation. In addition to this, ViewTransitions also wait up to 500ms for new fonts to load before starting the animation to avoid them flickering in later. For the same reason, an image wrapped in ViewTransition will wait for the image to load.

如果它位于一个新的 Suspense 边界实例内部,那么会先显示回退内容。在 Suspense 边界完全加载后,它会触发 <ViewTransition> 来动画显示内容。

🌐 If it’s inside a new Suspense boundary instance, then the fallback is shown first. After the Suspense boundary fully loads, it triggers the <ViewTransition> to animate the reveal to the content.

根据你放置 <ViewTransition> 的位置,有两种方法可以为 Suspense 边界添加动画:

🌐 There are two ways to animate Suspense boundaries depending on where you place the <ViewTransition>:

更新:

<ViewTransition>
<Suspense fallback={<A />}>
<B />
</Suspense>
</ViewTransition>

在这种情况下,当内容从 A 变为 B 时,它将被视为“更新”,并在适当时应用该类。A 和 B 都会获得相同的 view-transition-name,因此默认情况下它们会表现为交叉淡入淡出。

🌐 In this scenario when the content goes from A to B, it’ll be treated as an “update” and apply that class if appropriate. Both A and B will get the same view-transition-name and therefore they’re acting as a cross-fade by default.

import {ViewTransition, useState, startTransition, Suspense} from 'react';
import {Video, VideoPlaceholder} from './Video';
import {useLazyVideoData} from './data';

function LazyVideo() {
  const video = useLazyVideoData();
  return <Video video={video} />;
}

export default function Component() {
  const [showItem, setShowItem] = useState(false);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setShowItem((prev) => !prev);
          });
        }}>
        {showItem ? '➖' : '➕'}
      </button>
      {showItem ? (
        <ViewTransition>
          <Suspense fallback={<VideoPlaceholder />}>
            <LazyVideo />
          </Suspense>
        </ViewTransition>
      ) : null}
    </>
  );
}

进入/退出:

<Suspense fallback={<ViewTransition><A /></ViewTransition>}>
<ViewTransition><B /></ViewTransition>
</Suspense>

在这种情况下,这是两个独立的 ViewTransition 实例,每个实例都有自己的 view-transition-name。这将被视为 <A> 的“退出”和 <B> 的“进入”。

🌐 In this scenario, these are two separate ViewTransition instances each with their own view-transition-name. This will be treated as an “exit” of the <A> and an “enter” of the <B>.

根据你选择放置 <ViewTransition> 边界的位置,你可以实现不同的效果。

🌐 You can achieve different effects depending on where you choose to place the <ViewTransition> boundary.


选择退出动画

🌐 Opting-out of an animation

有时候你会封装一个大型的现有组件,比如整个页面,并且你想要为某些更新添加动画,比如更改主题。然而,你并不希望页面中的所有更新都选择在更新时进行交叉淡入。尤其是当你逐步添加更多动画时。

🌐 Sometimes you’re wrapping a large existing component, like a whole page, and you want to animate some updates, such as changing the theme. However, you don’t want it to opt-in all updates inside the whole page to cross-fade when they’re updating. Especially if you’re incrementally adding more animations.

你可以使用类“none”来选择不使用动画。通过将子元素封装在“none”中,你可以在父元素仍然触发动画的同时,禁用对子元素更新的动画。

🌐 You can use the class “none” to opt-out of an animation. By wrapping your children in a “none” you can disable animations for updates to them while the parent still triggers.

<ViewTransition>
<div className={theme}>
<ViewTransition update="none">{children}</ViewTransition>
</div>
</ViewTransition>

只有在主题变化时才会动画,而不是仅在子组件更新时动画。子组件仍然可以用它们自己的 <ViewTransition> 再次选择加入,但至少现在又是手动的了。

🌐 This will only animate if the theme changes and not if only the children update. The children can still opt-in again with their own <ViewTransition> but at least it’s manual again.


自定义动画

🌐 Customizing animations

默认情况下,<ViewTransition> 包括来自浏览器的默认交叉淡入淡出效果。

🌐 By default, <ViewTransition> includes the default cross-fade from the browser.

要自定义动画,你可以为 <ViewTransition> 组件提供属性,以根据 <ViewTransition> 的激活方式指定使用哪种动画。

🌐 To customize animations, you can provide props to the <ViewTransition> component to specify which animations to use, based on how the <ViewTransition> activates.

例如,我们可以减慢默认的淡入淡出动画:

🌐 For example, we can slow down the default cross fade animation:

<ViewTransition default="slow-fade">
<Video />
</ViewTransition>

并使用视图过渡类在 CSS 中定义慢淡入淡出效果:

🌐 And define slow-fade in CSS using view transition classes:

::view-transition-old(.slow-fade) {
animation-duration: 500ms;
}

::view-transition-new(.slow-fade) {
animation-duration: 500ms;
}
import {ViewTransition, useState, startTransition} from 'react';
import {Video} from './Video';
import videos from './data';

function Item() {
  return (
    <ViewTransition default="slow-fade">
      <Video video={videos[0]} />
    </ViewTransition>
  );
}

export default function Component() {
  const [showItem, setShowItem] = useState(false);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setShowItem((prev) => !prev);
          });
        }}>
        {showItem ? '➖' : '➕'}
      </button>

      {showItem ? <Item /> : null}
    </>
  );
}

除了设置 default,你还可以为 enterexitupdateshare 动画提供配置。

🌐 In addition to setting the default, you can also provide configurations for enter, exit, update, and share animations.

import {ViewTransition, useState, startTransition} from 'react';
import {Video} from './Video';
import videos from './data';

function Item() {
  return (
    <ViewTransition enter="slide-in" exit="slide-out">
      <Video video={videos[0]} />
    </ViewTransition>
  );
}

export default function Component() {
  const [showItem, setShowItem] = useState(false);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setShowItem((prev) => !prev);
          });
        }}>
        {showItem ? '➖' : '➕'}
      </button>

      {showItem ? <Item /> : null}
    </>
  );
}


使用类型自定义动画

🌐 Customizing animations with types

当特定激活触发器激活特定过渡类型时,你可以使用 addTransitionType API 向子元素添加类名。这允许你为每种类型的过渡自定义动画。

🌐 You can use the addTransitionType API to add a class name to the child elements when a specific transition type is activated for a specific activation trigger. This allows you to customize the animation for each type of transition.

例如,要自定义所有前进和后退导航的动画:

🌐 For example, to customize the animation for all forward and backward navigations:

<ViewTransition
default={{
'navigation-back': 'slide-right',
'navigation-forward': 'slide-left',
}}>
<div>...</div>
</ViewTransition>;

// in your router:
startTransition(() => {
addTransitionType('navigation-' + navigationType);
});

当 ViewTransition 激活“向后导航”动画时,React 会添加类名“slide-right”。当 ViewTransition 激活“向前导航”动画时,React 会添加类名“slide-left”。

🌐 When the ViewTransition activates a “navigation-back” animation, React will add the class name “slide-right”. When the ViewTransition activates a “navigation-forward” animation, React will add the class name “slide-left”.

未来,路由和其他库可能会添加对标准视图过渡类型和样式的支持。

🌐 In the future, routers and other libraries may add support for standard view-transition types and styles.

import {
  ViewTransition,
  addTransitionType,
  useState,
  startTransition,
} from 'react';
import {Video} from './Video';
import videos from './data';

function Item() {
  return (
    <ViewTransition
      enter={{
        'add-video-back': 'slide-in-back',
        'add-video-forward': 'slide-in-forward',
      }}
      exit={{
        'remove-video-back': 'slide-in-forward',
        'remove-video-forward': 'slide-in-back',
      }}>
      <Video video={videos[0]} />
    </ViewTransition>
  );
}

export default function Component() {
  const [showItem, setShowItem] = useState(false);
  return (
    <>
      <div className="button-container">
        <button
          onClick={() => {
            startTransition(() => {
              if (showItem) {
                addTransitionType('remove-video-back');
              } else {
                addTransitionType('add-video-back');
              }
              setShowItem((prev) => !prev);
            });
          }}>
          ⬅️
        </button>
        <button
          onClick={() => {
            startTransition(() => {
              if (showItem) {
                addTransitionType('remove-video-forward');
              } else {
                addTransitionType('add-video-forward');
              }
              setShowItem((prev) => !prev);
            });
          }}>
          ➡️
        </button>
      </div>
      {showItem ? <Item /> : null}
    </>
  );
}


用JavaScript进行动画制作

🌐 Animating with JavaScript

虽然 视图过渡类 允许你使用 CSS 定义动画,但有时你需要对动画进行命令式控制。onEnteronExitonUpdateonShare 回调为你提供对视图过渡伪元素的直接访问,以便你可以使用 Web 动画 API 对它们进行动画处理。

🌐 While View Transition Classes let you define animations with CSS, sometimes you need imperative control over the animation. The onEnter, onExit, onUpdate, and onShare callbacks give you direct access to the view transition pseudo-elements so you can animate them using the Web Animations API.

每个回调都会接收一个带有 .old.new 属性的 instance,这些属性表示视图过渡伪元素。你可以像对 DOM 元素一样调用它们的 .animate()

🌐 Each callback receives an instance with .old and .new properties representing the view transition pseudo-elements. You can call .animate() on them just like you would on a DOM element:

<ViewTransition
onEnter={(instance) => {
const anim = instance.new.animate(
[
{transform: 'scale(0.8)'},
{transform: 'scale(1)'},
],
{duration: 300, easing: 'ease-out'}
);
return () => anim.cancel();
}}>
<div>...</div>
</ViewTransition>

这使你能够结合 CSS 驱动的动画和 JavaScript 驱动的动画。

🌐 This allows you to combine CSS-driven animations and JavaScript-driven animations.

在以下示例中,默认的交叉淡入淡出由 CSS 处理,而幻灯片动画由 JavaScript 在 onEnteronExit 动画中驱动:

🌐 In the following example, the default cross-fade is handled by CSS, and the slide animations are driven by JavaScript in the onEnter and onExit animations:

import {ViewTransition, useState, startTransition} from 'react';
import {Video} from './Video';
import videos from './data';
import {SLIDE_IN, SLIDE_OUT} from './animations';

function Item() {
  return (
    <ViewTransition
      default="none"
      /* CSS driven cross fade defaults */
      enter="auto"
      exit="auto"
      /* JS driven slide animations */
      onEnter={(instance) => {
        const anim = instance.new.animate(
          SLIDE_IN,
          {duration: 500, easing: 'ease-out'}
        );
        return () => anim.cancel();
      }}
      onExit={(instance) => {
        const anim = instance.old.animate(
          SLIDE_OUT,
          {duration: 300, easing: 'ease-in'}
        );
        return () => anim.cancel();
      }}>
      <Video video={videos[0]} />
    </ViewTransition>
  );
}

export default function Component() {
  const [showItem, setShowItem] = useState(false);
  return (
    <>
      <button
        onClick={() => {
          startTransition(() => {
            setShowItem((prev) => !prev);
          });
        }}>
        {showItem ? '➖' : '➕'}
      </button>

      {showItem ? <Item /> : null}
    </>
  );
}

注意

始终清理视图转换事件

🌐 Always clean up View Transition Events

视图过渡事件应始终返回一个清理函数:

🌐 View Transition Events should always return a cleanup function:

<ViewTransition
onEnter={(instance) => {
const anim = instance.new.animate(
SLIDE_IN,
{duration: 500, easing: 'ease-out'}
);
return () => anim.cancel();
}}
>

这允许浏览器在视图过渡被中断时取消动画。

🌐 This allows the browser to cancel the animation when the View Transition is interrupted.


使用 JavaScript 动画过渡类型

🌐 Animating transition types with JavaScript

你可以使用传递给 ViewTransition 事件的 types 来根据过渡的触发方式有条件地应用不同的动画。

🌐 You can use types passed to ViewTransition events to conditionally apply different animations based on how the Transition was triggered.

<ViewTransition
onEnter={(instance, types) => {
const duration = types.includes('fast') ? 150 : 2000;
const anim = instance.new.animate(
SLIDE_IN,
{duration: duration, easing: 'ease-out'}
);
return () => anim.cancel();
}}
>

此示例调用 addTransitionType 将一个 Transition 标记为“快速”,然后调整动画持续时间:

🌐 This example calls addTransitionType to mark a Transition as “fast” and then adjust the animation duration:

import {ViewTransition, useState, startTransition, addTransitionType} from 'react';
import {Video} from './Video';
import videos from './data';
import {SLIDE_IN, SLIDE_OUT} from './animations';

function Item() {
  return (
    <ViewTransition
      onEnter={(instance, types) => {
        const duration = types.includes('fast') ? 150 : 2000;
        const anim = instance.new.animate(
          SLIDE_IN,
          {duration: duration, easing: 'ease-out'}
        );
        return () => anim.cancel();
      }}
      onExit={(instance, types) => {
        const duration = types.includes('fast') ? 150 : 500;
        const anim = instance.old.animate(
          SLIDE_OUT,
          {duration: duration, easing: 'ease-in'}
        );
        return () => anim.cancel();
      }}>
      <Video video={videos[0]} />
    </ViewTransition>
  );
}

export default function Component() {
  const [showItem, setShowItem] = useState(false);
  const [isFast, setIsFast] = useState(false);
  return (
    <>
      <div>
        Fast: <input type="checkbox" onChange={() => {setIsFast(f => !f)}} value={isFast}></input>
      </div><br />
      <button
        onClick={() => {
          startTransition(() => {
            if (isFast) {
              addTransitionType('fast');
            }
            setShowItem((prev) => !prev);
          });
        }}>
        {showItem ? '➖' : '➕'}
      </button>

      {showItem ? <Item /> : null}
    </>
  );
}


构建支持 View Transition 的路由

🌐 Building View Transition enabled routers

React 会等待任何挂起的导航完成,以确保滚动恢复发生在动画过程中。如果导航在 React 上被阻塞,你的路由必须在 useLayoutEffect 中解除阻塞,因为 useEffect 会导致死锁。

🌐 React waits for any pending Navigation to finish to ensure that scroll restoration happens within the animation. If the Navigation is blocked on React, your router must unblock in useLayoutEffect since useEffect would lead to a deadlock.

如果 startTransition 是从旧的 popstate 事件启动的,例如在“后退”导航期间,那么它必须同步完成,以确保滚动和表单恢复能够正常工作。这与运行视图过渡动画相冲突。因此,React 会跳过来自 popstate 的动画,并且动画不会在按下后退按钮时运行。你可以通过升级你的路由以使用导航 API 来解决这个问题。

🌐 If a startTransition is started from the legacy popstate event, such as during a “back”-navigation then it must finish synchronously to ensure scroll and form restoration works correctly. This is in conflict with running a View Transition animation. Therefore, React will skip animations from popstate and animations won’t run for the back button. You can fix this by upgrading your router to use the Navigation API.


故障排除

🌐 Troubleshooting

我的 <ViewTransition> 没有激活

🌐 My <ViewTransition> is not activating

<ViewTransition> 只有在放置在任何 DOM 节点之前才会激活:

function Component() {
return (
<div>
<ViewTransition>Hi</ViewTransition>
</div>
);
}

要修复此问题,请确保 <ViewTransition> 位于其他任何 DOM 节点之前:

🌐 To fix, ensure that the <ViewTransition> comes before any other DOM nodes:

function Component() {
return (
<ViewTransition>
<div>Hi</div>
</ViewTransition>
);
}

我收到一个错误:“有两个 <ViewTransition name=%s> 组件同时挂载并且名称相同。”

🌐 I’m getting an error “There are two <ViewTransition name=%s> components with the same name mounted at the same time.”

当两个具有相同 name<ViewTransition> 组件同时挂载时,会出现此错误:

🌐 This error occurs when two <ViewTransition> components with the same name are mounted at the same time:

function Item() {
// 🚩 All items will get the same "name".
return <ViewTransition name="item">...</ViewTransition>;
}

function ItemList({items}) {
return (
<>
{items.map((item) => (
<Item key={item.id} />
))}
</>
);
}

这将导致视图过渡出错。在开发过程中,React 会检测到此问题以提示,并记录两个错误:

🌐 This will cause the View Transition to error. In development, React detects this issue to surface it and logs two errors:

Console
There are two <ViewTransition name=%s> components with the same name mounted at the same time. This is not supported and will cause View Transitions to error. Try to use a more unique name e.g. by using a namespace prefix and adding the id of an item to the name. at Item at ItemList
The existing <ViewTransition name=%s> duplicate has this stack trace. at Item at ItemList

要修复此问题,请确保在整个应用中同时只挂载一个同名的 <ViewTransition>,方法是确保 name 是唯一的,或者在名称中添加 id

🌐 To fix, ensure that there’s only one <ViewTransition> with the same name mounted at a time in the entire app by ensuring the name is unique, or adding an id to the name:

function Item({id}) {
// ✅ All items will get a unique name.
return <ViewTransition name={`item-${id}`}>...</ViewTransition>;
}

function ItemList({items}) {
return (
<>
{items.map((item) => (
<Item key={item.id} item={item} />
))}
</>
);
}