view-transitions

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Animating View Transitions

实现View Transitions动画

Note: The View Transitions API for Single-Page Applications is available in Chrome 111+
注意:适用于单页应用的View Transitions API在Chrome 111及以上版本可用

When to Use

适用场景

  • Use this when you want to animate transitions between different page states or navigations
  • This is helpful for creating polished, app-like navigation experiences in web applications
  • 当你想要为不同页面状态或导航之间添加过渡动画时使用
  • 这有助于在Web应用中打造精致、类App的导航体验

Instructions

使用说明

  • Use
    document.startViewTransition(callback)
    to animate DOM changes
  • Assign unique
    view-transition-name
    CSS properties to elements that should transition between states
  • Check for browser support before using the API (
    if (document.startViewTransition)
    )
  • Minimize the time the DOM is frozen by starting transitions after data fetching completes
  • Consider CSS animation fallbacks for browsers that don't yet support the View Transitions API
  • 使用
    document.startViewTransition(callback)
    来为DOM变化添加动画
  • 为需要在状态间过渡的元素分配唯一的
    view-transition-name
    CSS属性
  • 使用前检查浏览器兼容性(
    if (document.startViewTransition)
  • 在数据获取完成后再启动过渡,尽量缩短DOM冻结的时间
  • 为尚不支持View Transitions API的浏览器考虑CSS动画降级方案

Details

详细内容

Introduction to View Transitions

View Transitions简介

The View Transitions API offers a simple way to transition any visual DOM change from one state to the next. This might include small changes such as toggling some content, or broader changes such as navigating from one page to the next.
The JavaScript API centers around
document.startViewTransition(callback)
, where
callback
is a function that typically updates the DOM to the new state.
Let's take toggling a
<details>
element as a simple example:
js
if (document.startViewTransition) {
  // (check for browser support)
  document.addEventListener("click", function (event) {
    if (event.target.matches("summary")) {
      event.preventDefault(); // (we'll toggle the element ourselves)
      const details = event.target.closest("details");
      document.startViewTransition(() => details.toggleAttribute("open"));
    }
  });
}
document.startViewTransition
takes a screenshot of the current DOM before calling the callback. Here, our callback just toggles the
open
attribute. Once complete, the browser can then transition between the initial screenshot and the new version.
These old and new versions are presented as pseudo elements and can be referenced in CSS with
::view-transition-old(root)
and
::view-transition-new(root)
respectively. For example, to emphasize the transition, we can lengthen the
animation-duration
like so:
css
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 2s;
}
View transitions are also capable of animating multiple changes with more advanced animations that go beyond the default crossfade. By giving specific elements a CSS
view-transition-name
, and a
containment
of
layout
or
paint
, the API gives developers granular control over how the elements transition, including their width, height, and position. These advanced transitions can really help communicate the flow from one page to the next.
Take a photo gallery as an example: the most obvious transition is the size and position of the photo, which is automatically achieved when the
<img>
element on each page is given the same unique
view-transition-name
, and a CSS
containment
value of
layout
. The
view-transition-name
s can be hard-coded in the style attributes, or added dynamically (e.g. in a
onclick
handler), as long as they're unique to the page and added before the transition is started.
The photo details beneath require a little more styling. We give each line element its own
view-transition-name
:
css
figcaption h2 {
  contain: layout;
  view-transition-name: photo-heading;
}
figcaption div {
  contain: layout;
  view-transition-name: photo-location-time;
}
figcaption dl {
  contain: layout;
  view-transition-name: photo-meta;
}
This generates transition groups for each area, which are just like the new/old screenshots mentioned earlier, but only cover an area of the page rather than the whole document. And just as the whole document transition elements could be targeted with
::view-transition-old(root)
and
::view-transition-new(root)
, these transition groups can be targeted with
::view-transition-old(NAME)
and
::view-transition-new(NAME)
. Note that the details text is not present on the photo grid page, therefore when transitioning from the grid to the photo page, there'll only be a
::view-transition-new(NAME)
, not a
::view-transition-old(NAME)
, and vice versa when navigating the other way. So we can target these cases using the
:only-child
pseudo class and customize the animation. For the
photo-heading
group:
css
/* Enter */
::view-transition-new(photo-heading):only-child {
  animation: 300ms ease 50ms both fade-in, 300ms ease 50ms both slide-up;
}

/* Exit */
::view-transition-old(photo-heading):only-child {
  animation: 200ms ease 150ms both fade-out, 200ms ease 150ms both slide-down;
}
That's the basics of the API. Jake Archibald's excellent View Transitions article covers the details well. For now, let's see how we might transition full page navigations.
View Transitions API提供了一种简单的方式,让任何视觉上的DOM变化都能从一个状态过渡到下一个状态。这可能包括切换内容等小变化,也可能是从一个页面导航到另一个页面的大范围变化。
JavaScript API围绕
document.startViewTransition(callback)
展开,其中
callback
是一个通常用于将DOM更新到新状态的函数。
我们以切换
<details>
元素为例,看一个简单的示例:
js
if (document.startViewTransition) {
  // (检查浏览器兼容性)
  document.addEventListener("click", function (event) {
    if (event.target.matches("summary")) {
      event.preventDefault(); // (我们将自行切换元素)
      const details = event.target.closest("details");
      document.startViewTransition(() => details.toggleAttribute("open"));
    }
  });
}
document.startViewTransition
会在调用回调函数前捕获当前DOM的截图。在这里,我们的回调函数只是切换
open
属性。完成后,浏览器就可以在初始截图和新版本之间进行过渡动画。
这些旧版本和新版本以伪元素的形式呈现,可以在CSS中分别用
::view-transition-old(root)
::view-transition-new(root)
来引用。例如,为了突出过渡效果,我们可以延长
animation-duration
css
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 2s;
}
View Transitions还能为多个变化添加更高级的动画,不止于默认的淡入淡出。通过为特定元素设置CSS的
view-transition-name
,以及
layout
paint
类型的
containment
,开发者可以精细控制元素的过渡方式,包括它们的宽度、高度和位置。这些高级过渡效果能有效传达页面间的流转关系。
以照片画廊为例:最明显的过渡是照片的大小和位置变化,只要给每个页面上的
<img>
元素设置相同的唯一
view-transition-name
,并将CSS的
containment
值设为
layout
,就能自动实现这个效果。
view-transition-name
可以硬编码在style属性中,也可以动态添加(比如在
onclick
处理函数中),只要在过渡开始前为页面设置唯一的名称即可。
照片下方的详情内容需要更多样式设置。我们给每个行元素设置自己的
view-transition-name
css
figcaption h2 {
  contain: layout;
  view-transition-name: photo-heading;
}
figcaption div {
  contain: layout;
  view-transition-name: photo-location-time;
}
figcaption dl {
  contain: layout;
  view-transition-name: photo-meta;
}
这会为每个区域生成过渡组,就像之前提到的新旧截图一样,但只覆盖页面的某个区域而非整个文档。就像整个文档的过渡元素可以用
::view-transition-old(root)
::view-transition-new(root)
来定位一样,这些过渡组可以用
::view-transition-old(NAME)
::view-transition-new(NAME)
来定位。注意,照片网格页面上没有详情文本,因此从网格页面过渡到照片页面时,只会有
::view-transition-new(NAME)
,而没有
::view-transition-old(NAME)
,反之亦然。所以我们可以使用
:only-child
伪类来定位这些情况,并自定义动画。以
photo-heading
组为例:
css
/* 进入动画 */
::view-transition-new(photo-heading):only-child {
  animation: 300ms ease 50ms both fade-in, 300ms ease 50ms both slide-up;
}

/* 退出动画 */
::view-transition-old(photo-heading):only-child {
  animation: 200ms ease 150ms both fade-out, 200ms ease 150ms both slide-down;
}
以上就是该API的基础内容。Jake Archibald撰写的优秀View Transitions文章详细介绍了相关细节。现在,我们来看看如何实现整页导航的过渡效果。

Page Navigations

页面导航

A typical page navigation looks something like:
  1. User clicks a link
  2. Request is made for data
  3. DOM is updated with the response
To apply a view transition in this flow, there are a couple of considerations.
First, is minimizing the time that the screen is in a frozen state. You may have noticed that once a view transition has started, the DOM will be not interactive until the callback completes. If we start the transition when the user clicks the link, they could be waiting a while with a frozen UI. To minimize this annoyance, ideally
document.startViewTransition
should be called after the request has completed. That way, we're ready for the change, and the DOM can be updated as swiftly as possible.
Second, we need to be sure the initial DOM screenshot has been captured before we update the DOM. When working with page navigations in third-party frameworks, we don't have full control over the rendering process; the DOM is automatically updated when the response is received. Therefore we don't have a standalone function we can pass to
document.startViewTransition
that will tidily perform the DOM update. We may need to intercept, pause, and resume rendering to give the illusion we have a single function that updates the DOM.
Nicely enough, if we return a promise from our DOM update callback, the view transition API will wait for its resolution before performing the animation. We can use this feature to handle the timing issues mentioned above.
典型的页面导航流程如下:
  1. 用户点击链接
  2. 发起数据请求
  3. 用响应内容更新DOM
要在这个流程中应用View Transition,需要考虑几点。
首先,要尽量缩短屏幕处于冻结状态的时间。你可能已经注意到,一旦View Transition开始,在回调函数完成前DOM将无法交互。如果我们在用户点击链接时就启动过渡,用户可能会在较长时间内面对冻结的UI。为了减少这种不便,理想情况下应该在请求完成后再调用
document.startViewTransition
。这样我们就可以随时更新DOM,尽可能快地完成DOM更新。
其次,我们需要确保在更新DOM前已经捕获了初始DOM的截图。在第三方框架中处理页面导航时,我们无法完全控制渲染过程;当收到响应时,DOM会自动更新。因此我们无法传递一个独立的函数给
document.startViewTransition
来完成DOM更新。我们可能需要拦截、暂停并恢复渲染,以营造出我们有一个单独的DOM更新函数的假象。
幸运的是,如果我们从DOM更新回调函数中返回一个Promise,View Transition API会等待Promise解析后再执行动画。我们可以利用这个特性来处理上述的时序问题。

React Component Example

React组件示例

To tackle the issues above, we'll create a React class component as it's easier to explain the flow compared to a functional component. We'll use the following lifecycle methods to control rendering:
  • shouldComponentUpdate
    : we'll return
    false
    here and start the view transition — this will buy us some time for the screenshot capture to complete
  • forceUpdate
    : to manually re-render the component after the screenshot capture
  • componentDidUpdate
    : to notify the view transition API that the DOM has updated
Here's how it looks:
js
import { Component } from "react";

export default class ViewTransition extends Component {
  shouldComponentUpdate() {
    if (!document.startViewTransition) return true; // skip when not supported

    document.startViewTransition(() => this.#updateDOM());
    return false; // don't update the component, we'll do this manually
  }

  #updateDOM() {
    // now we know the screenshot has been taken, we can force render
    // (which skips `shouldComponentUpdate`)
    this.forceUpdate();
    // set up a promise that will resolve when the component renders
    return new Promise((resolve) => {
      this.#rendered = resolve;
    });
  }

  render() {
    return this.props.children;
  }

  #rendered = () => {};

  componentDidUpdate() {
    // resolve the `updateDOM` promise to notify the View Transition API
    // that the DOM has been updated
    this.#rendered();
  }
}
Note: The Next.js App Router is in beta at the time of writing and best-practices around it and the pages directory may be subject to change.
To use this in a Next.js app, first we'll disable React strict mode in development. Strict mode runs its checks by rendering the component twice. This interferes with the
ViewTransition
rendering flow in development so we'll disable it globally and re-enable it for child components with the
StrictMode
component.
js
// next.config.js
const nextConfig = {
  reactStrictMode: false,
};

module.exports = nextConfig;
Next, in
pages/_app.js
, we'll wrap
Component
in our
ViewTransition
and
StrictMode
component, and we should begin to see animated transitions:
js
// pages/_app.js
import "@/styles/globals.css";
import { StrictMode } from "react";
import ViewTransition from "@/components/ViewTransition";

export default function App({ Component, pageProps }) {
  return (
    <ViewTransition>
      <StrictMode>
        <Component {...pageProps} />
      </StrictMode>
    </ViewTransition>
  );
}
Note: the React documentation advises against using
shouldComponentUpdate
and
forceUpdate
, stating they should only be used for performance optimizations, and that
shouldComponentUpdate
is not guaranteed to be called. As page animations are an enhancement, and this component will work even if
shouldComponentUpdate
is not called, this caveat is acceptable.
为了解决上述问题,我们将创建一个React类组件,因为相比函数组件,它更容易解释流程。我们将使用以下生命周期方法来控制渲染:
  • shouldComponentUpdate
    :我们在这里返回
    false
    并启动View Transition——这将为我们争取时间完成截图捕获
  • forceUpdate
    :在截图捕获完成后手动重新渲染组件
  • componentDidUpdate
    :通知View Transition API DOM已更新
代码如下:
js
import { Component } from "react";

export default class ViewTransition extends Component {
  shouldComponentUpdate() {
    if (!document.startViewTransition) return true; // 不支持时跳过

    document.startViewTransition(() => this.#updateDOM());
    return false; // 不更新组件,我们将手动处理
  }

  #updateDOM() {
    // 现在我们知道截图已捕获,可以强制渲染
    // (这会跳过`shouldComponentUpdate`)
    this.forceUpdate();
    // 创建一个Promise,在组件渲染完成后解析
    return new Promise((resolve) => {
      this.#rendered = resolve;
    });
  }

  render() {
    return this.props.children;
  }

  #rendered = () => {};

  componentDidUpdate() {
    // 解析`updateDOM`中的Promise,通知View Transition API
    // DOM已更新
    this.#rendered();
  }
}
注意:撰写本文时,Next.js App Router处于测试阶段,关于它和pages目录的最佳实践可能会有所变化。
要在Next.js应用中使用这个组件,首先我们需要在开发环境中禁用React严格模式。严格模式会通过两次渲染组件来运行检查,这会干扰开发环境中
ViewTransition
的渲染流程,因此我们将全局禁用它,并通过
StrictMode
组件为子组件重新启用。
js
// next.config.js
const nextConfig = {
  reactStrictMode: false,
};

module.exports = nextConfig;
接下来,在
pages/_app.js
中,我们将
Component
包裹在
ViewTransition
StrictMode
组件中,这样就能看到动画过渡效果了:
js
// pages/_app.js
import "@/styles/globals.css";
import { StrictMode } from "react";
import ViewTransition from "@/components/ViewTransition";

export default function App({ Component, pageProps }) {
  return (
    <ViewTransition>
      <StrictMode>
        <Component {...pageProps} />
      </StrictMode>
    </ViewTransition>
  );
}
注意:React文档建议避免使用
shouldComponentUpdate
forceUpdate
,指出它们仅应用于性能优化,且
shouldComponentUpdate
并不保证一定会被调用。由于页面动画是一种增强功能,即使
shouldComponentUpdate
未被调用,该组件仍能正常工作,因此这个警告是可以接受的。

An Alternative Approach without View Transitions

不使用View Transitions的替代方案

One necessary downside of the View Transitions API for page transitions is that it needs the new page HTML before animating. This can take time and leaves the user without any feedback after they click a link. A spinner may fill the gap, but we could buy some time by animating out elements as soon as the user clicks a link, then animate in the new HTML when it arrives. This is similar to how standard iOS navigations slide across immediately whilst loading the next screen.
  1. User clicks a link
  2. Elements are animated out; meanwhile the request is made for data
  3. Wait for both the response and the animations to complete
  4. Animate in the response
The main difference between this approach and that of the View Transitions API, is that it can't transition elements between one state to the next because at the time it animates out, it doesn't have the new HTML in order to do so.
Both approaches are useful depending on the situation. For example, if there are shared elements from one page to the next, you might opt for a view transition, whereas if the change is significant with few shared elements, you could benefit from the immediate feedback of an exit animation.
To implement this, we'll need to hook into routing events, which will depend on the framework or library you're using. In particular, we'll need to be notified when the user navigates. With Next.js, we can use the
routeChangeStart
router event
to start the exit animations, but let's look at how we might achieve this without Next.js, React, or fully client-rendered HTML.
使用View Transitions API实现页面过渡的一个必要缺点是,它需要先获取新页面的HTML才能进行动画。这可能需要一定时间,导致用户点击链接后没有任何反馈。加载指示器可以填补这个空白,但我们可以在用户点击链接后立即将元素动画退出,然后在新HTML到达时将其动画进入,以此争取时间。这类似于标准iOS导航的方式:立即滑动切换,同时加载下一个屏幕。
  1. 用户点击链接
  2. 元素动画退出;同时发起数据请求
  3. 等待响应和动画完成
  4. 将响应内容动画进入
这种方法与View Transitions API的主要区别在于,它无法将元素从一个状态过渡到下一个状态,因为在动画退出时,它还没有新的HTML来完成过渡。
两种方法各有用途,具体取决于场景。例如,如果页面之间有共享元素,你可能会选择View Transition;而如果变化较大且共享元素很少,那么立即显示退出动画的即时反馈会更有优势。
要实现这种方法,我们需要监听路由事件,这取决于你使用的框架或库。特别是,我们需要在用户导航时收到通知。在Next.js中,我们可以使用
routeChangeStart
路由事件
来启动退出动画,但我们来看看如何在不使用Next.js、React或完全客户端渲染HTML的情况下实现这一点。

Animating Server-side Rendered Multi-page Applications with Turbo and Turn

使用Turbo和Turn为服务端渲染的多页应用添加动画

Note: There are plans for the View Transition API to work for multi-page navigations, i.e. without JavaScript. However, the JavaScript API may still be needed for more advanced transitions.
Turbo, part of the Hotwire suite of libraries (not to be confused with Vercel's Turbo), offers a rendering approach that progressively enhances multi-page applications (MPAs). It aims to achieve SPA speeds without having to architect your code as a fully client-rendered application, and does so by capturing link clicks and form submissions, performing the request with JavaScript, and replacing the
<body>
with the new
<body>
from the response. In this way, it's a hybrid approach: the HTML is generated on the server, but the DOM is updated via JavaScript.
Turn is a library for animating page navigations using Turbo. It supports both animation approaches (although currently view transitions are experimental). Turn adds
turn-before-exit
,
turn-exit
, and
turn-enter
classes to the
<html>
element at the appropriate times, providing a way for developers to customize the animations.
To get it working, add
data-turn-exit
and
data-turn-enter
attributes to the elements you wish to animate, then apply your CSS styles. For example, for a fade-in/fade-out:
css
html.turn-exit [data-turn-exit] {
  animation-name: fade-out;
  animation-duration: 0.3s;
  animation-fill-mode: forwards;
}

html.turn-enter [data-turn-enter] {
  animation-name: fade-in;
  animation-duration: 0.6s;
  animation-fill-mode: forwards;
}

@keyframes fade-out {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

@keyframes fade-in {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}
Then import the
Turn
library into your application's JavaScript and call
Turn.start()
.
It works by hooking into Turbo's rendering events, and controlling the flow as needed:
  1. turbo:visit
    : just before the request starts, add the
    turn-exit
    class
  2. turbo:before-render
    : after the request has completed but before the new HTML renders (similar to React's
    shouldComponentUpdate
    ), pause rendering to wait for any exit animations to complete
  3. turbo:render
    : once the new HTML has been rendered, remove
    turn-exit
    class and add the
    turn-enter
    class
  4. once the exit animations complete, remove the
    turn-enter
    class
Turn also has experimental support for view transitions, enabled by setting
Turn.config.experimental.viewTransitions = true
. This will use view transitions where supported, and fallback to the CSS animation approach.
注意:计划让View Transition API支持多页导航,即无需JavaScript。不过,更高级的过渡效果可能仍需要JavaScript API。
TurboHotwire库套件的一部分(不要与Vercel的Turbo混淆),它提供了一种渐进式增强多页应用(MPA)的渲染方式。它旨在无需将代码架构为完全客户端渲染的应用,就能实现SPA的速度,具体方式是捕获链接点击和表单提交,用JavaScript发起请求,并用响应中的新
<body>
替换当前的
<body>
。这是一种混合方法:HTML在服务器端生成,但DOM通过JavaScript更新。
Turn是一个使用Turbo实现页面导航动画的库。它支持两种动画方法(尽管目前View Transitions处于实验阶段)。Turn会在适当的时候为
<html>
元素添加
turn-before-exit
turn-exit
turn-enter
类,为开发者提供自定义动画的方式。
要使用它,为想要动画的元素添加
data-turn-exit
data-turn-enter
属性,然后应用CSS样式。例如,实现淡入淡出效果:
css
html.turn-exit [data-turn-exit] {
  animation-name: fade-out;
  animation-duration: 0.3s;
  animation-fill-mode: forwards;
}

html.turn-enter [data-turn-enter] {
  animation-name: fade-in;
  animation-duration: 0.6s;
  animation-fill-mode: forwards;
}

@keyframes fade-out {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

@keyframes fade-in {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}
然后将
Turn
库导入到应用的JavaScript中,并调用
Turn.start()
它的工作原理是监听Turbo的渲染事件,并根据需要控制流程:
  1. turbo:visit
    :在请求开始前,添加
    turn-exit
  2. turbo:before-render
    :请求完成后但新HTML渲染前(类似于React的
    shouldComponentUpdate
    ),暂停渲染以等待退出动画完成
  3. turbo:render
    :新HTML渲染完成后,移除
    turn-exit
    类并添加
    turn-enter
  4. 退出动画完成后,移除
    turn-enter
Turn还对View Transitions提供实验性支持,通过设置
Turn.config.experimental.viewTransitions = true
启用。这将在支持的浏览器中使用View Transitions,在不支持的浏览器中降级为CSS动画方式。

Summary

总结

Page transitions can be a great way to communicate changes from one page to the next. The new built-in View Transitions API can perform complex transitions when provided with the old and new states. By hooking into framework events, we can communicate to the API these state changes. For page navigations, ideally the transitions should occur after the request has finished to avoid the DOM being in an inactive state.
An alternative (or complementary) approach is to perform exit animations immediately, as soon as the user has clicked a link. This has the benefit of buying some time for the request to complete before the new HTML arrives.
页面过渡是传达页面间变化的绝佳方式。新的内置View Transitions API在提供新旧状态时可以实现复杂的过渡效果。通过监听框架事件,我们可以向API传达这些状态变化。对于页面导航,理想情况下应在请求完成后再进行过渡,以避免DOM处于非活动状态。
另一种(或补充)方法是在用户点击链接后立即执行退出动画。这样做的好处是在新HTML到达前为请求完成争取时间。

Source

来源