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

Experimental Feature

该 API 是实验性的,尚未在 React 的稳定版本中提供。

¥This API is experimental and is not available in a stable version of React yet.

你可以通过将 React 包升级到最新的实验版本来尝试:

¥You can try it by upgrading React packages to the most recent experimental version:

  • react@experimental

  • react-dom@experimental

  • eslint-plugin-react-hooks@experimental

React 的实验版本可能包含错误。不要在生产中使用它们。

¥Experimental versions of React may contain bugs. Don’t use them in production.

<ViewTransition> 允许你为在 Transition 内更新的元素添加动画。

¥<ViewTransition> lets you animate elements that update inside a Transition.

import {unstableViewTransition as ViewTransition} from 'react';

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

参考

¥Reference

<ViewTransition>

将元素封装在 <ViewTransition> 中,以便在它们在 转换 内部更新时为其设置动画。React 使用以下启发式方法来确定动画是否激活视图过渡:

¥Wrap elements in <ViewTransition> to animate them when they update inside a Transition. React uses the following heuristics to determine if a View Transition activates for an animation:

  • enter:如果 ViewTransition 本身插入到此 Transition 中,则此 Transition 将会激活。

    ¥enter: If a ViewTransition itself gets inserted in this Transition, then this will activate.

  • exit:如果 ViewTransition 本身在此 Transition 中被删除,则这将被激活。

    ¥exit: If a ViewTransition itself gets deleted in this Transition, then this will activate.

  • update:如果 ViewTransition 内部有任何 React 正在进行的 DOM 突变(例如属性更改),或者 ViewTransition 边界本身由于其直属兄弟组件而改变了大小或位置。如果有嵌套的 ViewTransition 节点,则突变将应用于它们,而不是父节点。

    ¥update: If a ViewTransition has any DOM mutations inside it that React is doing (such as a prop changing) or if the ViewTransition boundary itself changes size or position due to an immediate sibling. If there are nested ViewTransition then the mutation applies to them and not the parent.

  • share:如果一个名为 ViewTransition 的 Transition 位于已删除的子树中,而另一个同名的 ViewTransition 位于同一 Transition 中已插入的子树中,则它们将组成一个共享元素 Transition,并从已删除的 Transition 动画到已插入的 Transition。

    ¥share: If a named ViewTransition is inside a deleted subtree and another named ViewTransition with the same name is part of an inserted subtree in the same Transition, they form a Shared Element Transition, and it animates from the deleted one to the inserted one.

默认情况下,<ViewTransition> 动画采用平滑的交叉淡入淡出(浏览器默认视图转换)。你可以通过向 <ViewTransition> 组件提供 视图过渡类 来自定义动画。你可以为每种触发器自定义动画(参见 视图过渡样式)。

¥By default, <ViewTransition> animates with a smooth cross-fade (the browser default view transition). You can customize the animation by providing a View Transition Class to the <ViewTransition> component. You can customize animations for each kind of trigger (see Styling View Transitions).

深入研究

<ViewTransition> 如何工作?

¥How does <ViewTransition> work?

在底层,React 将 view-transition-name 应用于嵌套在 <ViewTransition> 组件内的最近 DOM 节点的内联样式。如果有多个像 <ViewTransition><div /><div /></ViewTransition> 这样的兄弟 DOM 节点,React 会在名称后添加后缀,使每个节点都具有唯一性,但从概念上讲,它们是同一个节点的一部分。React 不会立即应用这些参数,而只会在边界应该参与动画时才应用。

¥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 本身来协调这些操作。如果你过去有其他触发 ViewTransitions 的方式,我们建议你迁移到内置方式。

¥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 of trigger ViewTransitions in the past, we recommend that you migrate to the built-in way.

如果其他 React ViewTransition 已在运行,则 React 将等待它们完成后再启动下一个。但是,重要的是,如果在第一个更新运行时发生了多个更新,则所有更新都将合并为一个更新。如果从 A 开始,则为 。然后与此同时,你会收到一个更新,转到 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 life-cycle 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 并调用 useInsertionEffects。

    ¥Apply its mutations to the DOM and invoke useInsertionEffects.

  • 等待字体加载。

    ¥Wait for fonts to load.

  • 调用 componentDidMount、componentDidUpdate、useLayoutEffect 和引用。

    ¥Call componentDidMount, componentDidUpdate, useLayoutEffect and refs.

  • 等待任何待处理的导航完成。

    ¥Wait for any pending Navigation to finish.

  • 然后 React 将测量布局的任何变化,以确定哪些边界需要动画。

    ¥Then React will measure any changes to the layout to see which boundaries will need to animate.

startViewTransition 的就绪 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

默认情况下,<ViewTransition> 动画采用平滑的交叉淡入淡出。你可以使用以下属性自定义动画或指定共享元素过渡:

¥By default, <ViewTransition> animates with a smooth cross-fade. You can customize the animation, or specify a shared element transition, with these props:

  • 可选 enter:字符串或对象。进入激活时应用的 视图过渡类

    ¥optional enter: A string or object. The View Transition Class to apply when enter is activated.

  • 可选 exit:字符串或对象。退出激活时应用的 视图过渡类

    ¥optional exit: A string or object. The View Transition Class to apply when exit is activated.

  • 可选 update:字符串或对象。更新激活时应用的 视图过渡类

    ¥optional update: A string or object. The View Transition Class to apply when an update is activated.

  • 可选 share:字符串或对象。共享元素激活时应用的 视图过渡类

    ¥optional share: A string or object. The View Transition Class to apply when a shared element is activated.

  • 可选 default:字符串或对象。未找到其他匹配的激活属性时使用的 视图过渡类

    ¥optional default: A string or object. The View Transition Class used when no other matching activation prop is found.

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

    ¥optional name: A string or object. The name of the View Transition used for shared element transitions. If not provided, React will use a unique name for each View Transition to prevent unexpected animations.

回调函数

¥Callback

这些回调允许你使用 animate API 强制调整动画:

¥These callbacks allow you to adjust the animation imperatively using the animate APIs:

  • 可选 onEnter:一个功能。React 在 “enter” 动画之后调用 onEnter

    ¥optional onEnter: A function. React calls onEnter after an “enter” animation.

  • 可选 onExit:一个功能。React 在 “exit” 动画之后调用 onExit

    ¥optional onExit: A function. React calls onExit after an “exit” animation.

  • 可选 onShare:一个功能。React 在 “share” 动画之后调用 onShare

    ¥optional onShare: A function. React calls onShare after a “share” animation.

  • 可选 onUpdate:一个功能。React 在 “update” 动画之后调用 onUpdate

    ¥optional onUpdate: A function. React calls onUpdate after an “update” animation.

每个回调接收的参数为:

¥Each callback receives as arguments:

  • element:已添加动画效果的 DOM 元素。

    ¥element: The DOM element that was animated.

  • types:动画中包含的 过渡类型

    ¥types: The Transition Types included in the animation.

视图过渡类

¥View Transition Class

View Transition Class 是 React 在 ViewTransition 激活时在过渡期间应用的 CSS 类名。它可以是字符串或对象。

¥The View Transition Class is the CSS class name(s) applied by React during the transition when the ViewTransition activates. It can be a string or an object.

  • string:激活时添加到子元素上的 class。如果提供了 'none',则不会添加任何类。

    ¥string: the class added on the child elements when activated. If 'none' is provided, no class will be added.

  • object:添加到子元素上的类将是与 addTransitionType 添加的 View Transition 类型匹配的关键。如果未找到匹配的类型,对象还可以指定要使用的 default

    ¥object: the class added on the child elements will be the key matching View Transition type added with addTransitionType. The object can also specify a default to use if no matching type is found.

'none' 值可用于阻止视图过渡因特定触发器而激活。

¥The value 'none' can be used to prevent a View Transition from activating for a specific trigger.

视图过渡样式

¥Styling View Transitions

注意

在网络上许多早期的 View Transition 示例中,你会看到使用 view-transition-name,然后使用 ::view-transition-...(my-name) 选择器设置其样式。我们不建议将其用于样式设置。我们通常建议使用 View Transition 类。

¥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> 的动画,你可以为其中一个激活属性提供一个 View Transition 类。View Transition Class 是 React 在 ViewTransition 激活时应用于子元素的 CSS 类名。

¥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” 动画,请为 enter 属性提供一个类名:

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

<ViewTransition enter="slide-in">

<ViewTransition> 激活 “enter” 动画时,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.

注意事项

¥Caveats

  • 默认情况下,setState 会立即更新,不会激活 <ViewTransition>,只会更新封装在 转换 中的组件。你还可以使用 <Suspense> 选择将 Transition 转换为 显示内容

    ¥By default, setState updates immediately and does not activate <ViewTransition>, only updates wrapped in a Transition. You can also use <Suspense> to opt-in to a Transition to reveal content.

  • <ViewTransition> 创建一个可以移动、缩放和淡入淡出的图片。与你在 React Native 或 Motion 中看到的布局动画不同,这意味着并非其中的每个元素都会为其位置设置动画。与为每个部分单独设置动画相比,这可以带来更好的性能和更连续的感觉,动画也更流畅。但是,它也可能会使应该自行移动的元素失去连续性。因此,你可能需要手动添加更多 <ViewTransition> 边界。

    ¥<ViewTransition> creates an image that can be moved around, scaled and cross-faded. Unlike Layout Animations you may have seen in React Native or Motion, this means that not every individual Element inside of it animates its position. This can lead to better performance and a more continuous feeling, smooth animation compared to animating every individual piece. However, it can also lose continuity in things that should be moving by themselves. So you might have to add more <ViewTransition> boundaries manually as a result.

  • 许多用户可能更喜欢页面上没有动画。在这种情况下,React 不会自动禁用动画。我们建议使用 @media (prefers-reduced-motion) 媒体查询来禁用动画,或者根据用户偏好降低动画效果。未来,CSS 库可能会将其内置到其预设中。

    ¥Many users may prefer not having animations on the page. React doesn’t automatically disable animations for this case. We recommend that using the @media (prefers-reduced-motion) media query to disable animations or tone them down based on user preference. In the future, CSS libraries may have this built-in to their presets.

  • 目前,<ViewTransition> 仅在 DOM 中起作用。我们正在努力添加对 React Native 和其他平台的支持。

    ¥Currently, <ViewTransition> only works in the DOM. We’re working on adding support for React Native and other platforms.


用法

¥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>Hi</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 {
  unstable_ViewTransition as ViewTransition,
  useState,
  startTransition
} from 'react';
import {Video} from "./Video";
import videos from "./data"

function Item() {
  return (
    <ViewTransition>
      <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}
    </>
  );
}

易犯错误

<ViewTransition> 仅在放置在任何 DOM 节点之前时才会激活。如果 Child 看起来像这样,则不会触发任何动画:

¥<ViewTransition> only activates if it is placed before any DOM node. If Child instead looked like this, no animation would trigger:

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

为共享元素添加动画

¥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>

当一个组件树卸载而另一个组件树挂载时,如果在卸载树和挂载树中存在同名组件,则它们会在两者上触发 “share” 动画。它会从卸载端动画到挂载端。

¥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> 也符合退出/进入的条件,则 “share” 动画优先。

¥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 {
  unstable_ViewTransition as 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))}
  />
}

注意

如果一对 Transition 中已挂载或未挂载的一方位于视口之外,则不会形成任何对。这确保在滚动某些内容时,它不会飞入或飞出视口。相反,它会被视为一个常规的进入/退出操作。

¥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.

如果同一组件实例的位置发生变化,从而触发 “update”,则不会发生这种情况。无论某个位置是否超出视口,这些动画都会执行。

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

目前存在一个怪癖:如果深度嵌套的未挂载 <ViewTransition> 位于视口内,但挂载侧不在视口内,则即使未挂载侧是深度嵌套的,它也会作为自己的 “exit” 动画进行动画处理,而不是作为父动画的一部分。

¥There’s currently a quirk 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 节点之外,则会触发 “update” 动画。类似于进入/退出动画。

¥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 {
  unstable_ViewTransition as 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 {
  unstable_ViewTransition as 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

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

¥Just 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.

目前,这仅适用于客户端过渡。未来,当服务器内容在初始加载期间暂停时,这还将为流式 SSR 的 Suspense 边界设置动画。

¥Currently, this only happens for client-side Transition. In the future, this will also animate Suspense boundary for streaming SSR when content from the server suspends during the initial load.

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

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

更新:

¥Update:

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

在这种情况下,当内容从 A 移动到 B 时,它将被视为 “update”,并在适当的情况下应用该类。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 {
  unstable_ViewTransition as 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}
    </>
  );
}

进入/退出:

¥Enter/Exit:

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

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

¥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 {
  unstable_ViewTransition as 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 {
  unstable_ViewTransition as 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 激活 “navigation-back” 动画时,React 会添加类名 “slide-right”。当 ViewTransition 激活 “navigation-forward” 动画时,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 {
  unstable_ViewTransition as ViewTransition,
  unstable_addTransitionType as 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}
    </>
  );
}

构建支持 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 事件启动的,例如在 “back” 导航期间,则它必须同步完成以确保滚动和表单恢复正常工作。这与运行视图过渡动画冲突。因此,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. Therefore 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 节点之前时才会激活:

¥<ViewTransition> only activates if it is placed is before any DOM node:

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 (
<>
{item.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
有两个同名的 ,[object Object], 组件同时挂载。此方法不受支持,会导致视图过渡错误。尝试使用更独特的名称,例如使用命名空间前缀并在名称中添加项目的 id。, , 位于 Item 中 , , 位于 ItemList 中¥There are two ,[object Object], 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
现有的 ,[object Object], 副本包含以下堆栈跟踪。, , 位于 Item 中 , , 位于 ItemList 中¥The existing ,[object Object], 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 the same "name".
return <ViewTransition name={`item-${id}`}>...</ViewTransition>;
}

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