适用于单页应用的同文档视图转换

发布时间:2021 年 8 月 17 日;上次更新时间:2024 年 9 月 25 日

当视图转换在单个文档上运行时,称为同文档视图转换。在单页应用 (SPA) 中,通常会使用 JavaScript 来更新 DOM。自 Chrome 111 起,Chrome 支持同文档视图过渡。

如需触发同文档视图过渡,请调用 document.startViewTransition

function handleClick(e) {  // Fallback for browsers that don't support this API:  if (!document.startViewTransition) {  updateTheDOMSomehow();  return;  }  // With a View Transition:  document.startViewTransition(() => updateTheDOMSomehow()); } 

调用时,浏览器会自动捕获声明了 view-transition-name CSS 属性的所有元素的快照。

然后,它会执行传入的回调函数来更新 DOM,之后会拍摄新状态的快照。

然后,这些快照会排列在伪元素的树中,并使用 CSS 动画的功能进行动画处理。旧状态和新状态的快照对从旧位置和大小平稳过渡到新位置,同时其内容会淡入淡出。如果您愿意,可以使用 CSS 自定义动画。


默认过渡效果:淡入淡出

默认视图过渡效果是淡入淡出,因此它非常适合作为 API 的入门示例:

function spaNavigate(data) {  // Fallback for browsers that don't support this API:  if (!document.startViewTransition) {  updateTheDOMSomehow(data);  return;  }  // With a transition:  document.startViewTransition(() => updateTheDOMSomehow(data)); } 

其中 updateTheDOMSomehow 将 DOM 更改为新状态。您可以随意选择付款方式。例如,您可以添加或移除元素、更改类名称或更改样式。

这样一来,页面就会交叉淡入淡出:

默认的淡入淡出效果。极简演示来源

好吧,淡入淡出效果并不那么令人印象深刻。幸运的是,您可以自定义过渡效果,但首先需要了解这种基本的淡入淡出效果是如何实现的。


这些过渡效果的运作方式

我们来更新之前的代码示例。

document.startViewTransition(() => updateTheDOMSomehow(data)); 

当调用 .startViewTransition() 时,API 会捕获网页的当前状态。这包括拍摄快照。

完成后,系统会调用传递给 .startViewTransition() 的回调。这就是 DOM 发生变化的地方。然后,API 会捕获网页的新状态。

捕获新状态后,API 会构建如下所示的伪元素树:

::view-transition └─ ::view-transition-group(root)   └─ ::view-transition-image-pair(root)   ├─ ::view-transition-old(root)   └─ ::view-transition-new(root) 

::view-transition 位于叠加层中,覆盖页面上的所有其他内容。如果您想为过渡设置背景颜色,此属性会非常有用。

::view-transition-old(root) 是旧视图的屏幕截图,::view-transition-new(root) 是新视图的实时表示形式。两者都呈现为 CSS“替换内容”(如 <img>)。

旧视图从 opacity: 1 动画化为 opacity: 0,而新视图从 opacity: 0 动画化为 opacity: 1,从而创建淡入淡出效果。

所有动画均使用 CSS 动画执行,因此可以使用 CSS 进行自定义。

自定义过渡效果

所有视图过渡伪元素都可以通过 CSS 进行定位,并且由于动画是使用 CSS 定义的,因此您可以使用现有的 CSS 动画属性来修改它们。例如:

::view-transition-old(root), ::view-transition-new(root) {  animation-duration: 5s; } 

进行此更改后,淡入效果现在非常缓慢:

长淡入淡出。极简演示来源

好吧,这仍然不够令人印象深刻。以下代码实现了 Material Design 的共享轴过渡效果

@keyframes fade-in {  from { opacity: 0; } } @keyframes fade-out {  to { opacity: 0; } } @keyframes slide-from-right {  from { transform: translateX(30px); } } @keyframes slide-to-left {  to { transform: translateX(-30px); } } ::view-transition-old(root) {  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,  300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; } ::view-transition-new(root) {  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,  300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; } 

结果如下:

共享轴过渡效果。极简演示来源

过渡多个元素

在之前的演示中,整个网页都参与了共享轴转场。这适用于网页的大部分内容,但对于标题来说似乎不太合适,因为标题会先滑出,然后再滑回。

为避免这种情况,您可以从页面的其余部分提取标题,以便单独设置动画效果。为此,请为元素分配 view-transition-name

.main-header {  view-transition-name: main-header; } 

view-transition-name 的值可以是您想要的任何值(none 除外,因为 none 表示没有过渡名称)。用于在过渡期间唯一标识元素。

结果如下:

具有固定标题的共享轴过渡。极简演示来源

现在,标题会保持在原位并进行淡入淡出。

该 CSS 声明导致伪元素树发生变化:

::view-transition ├─ ::view-transition-group(root) │ └─ ::view-transition-image-pair(root) │ ├─ ::view-transition-old(root) │ └─ ::view-transition-new(root) └─ ::view-transition-group(main-header)   └─ ::view-transition-image-pair(main-header)   ├─ ::view-transition-old(main-header)   └─ ::view-transition-new(main-header) 

现在有两个过渡组。一个用于标题,另一个用于其余部分。这些元素可以通过 CSS 单独定位,并赋予不同的过渡效果。不过,在本例中,main-header 留下了默认过渡效果,即淡入淡出。

好吧,默认过渡不仅仅是交叉淡化,::view-transition-group 也会过渡:

  • 定位和转换(使用 transform
  • 宽度
  • 高度

在目前为止,这并不重要,因为在 DOM 更改的两侧,标题的大小和位置都相同。不过,您也可以提取标题中的文字:

.main-header-text {  view-transition-name: main-header-text;  width: fit-content; } 

使用 fit-content 可使元素的大小与文本大小相同,而不是拉伸到剩余宽度。如果没有此设置,返回箭头会缩小标题文本元素的大小,而不是在两个页面中保持相同的大小。

现在,我们有三个部分可供探索:

::view-transition ├─ ::view-transition-group(root) │ └─ … ├─ ::view-transition-group(main-header) │ └─ … └─ ::view-transition-group(main-header-text)   └─ … 

不过,我们还是使用默认值:

滑动标题文字。极简演示来源

现在,标题文字会顺畅地滑动,为返回按钮腾出空间。


使用 view-transition-class 以相同方式为多个伪元素添加动画效果

Browser Support

  • Chrome: 125.
  • Edge: 125.
  • Firefox Technology Preview: supported.
  • Safari: 18.2.

Source

假设您有一个包含多个卡片的视图过渡,但页面上也有一个标题。如需为除标题之外的所有卡片添加动画效果,您必须编写一个选择器,用于定位每个单独的卡片。

h1 {  view-transition-name: title; } ::view-transition-group(title) {  animation-timing-function: ease-in-out; } #card1 { view-transition-name: card1; } #card2 { view-transition-name: card2; } #card3 { view-transition-name: card3; } #card4 { view-transition-name: card4; }  #card20 { view-transition-name: card20; } ::view-transition-group(card1), ::view-transition-group(card2), ::view-transition-group(card3), ::view-transition-group(card4),  ::view-transition-group(card20) {  animation-timing-function: var(--bounce); } 

有 20 个元素?也就是说,您需要编写 20 个选择器。添加新元素?然后,您还需要扩大应用动画样式的选择器。可扩缩性不太好。

view-transition-class 可用于视图过渡伪元素,以应用相同的样式规则。

#card1 { view-transition-name: card1; } #card2 { view-transition-name: card2; } #card3 { view-transition-name: card3; } #card4 { view-transition-name: card4; } #card5 { view-transition-name: card5; }  #card20 { view-transition-name: card20; } #cards-wrapper > div {  view-transition-class: card; } html::view-transition-group(.card) {  animation-timing-function: var(--bounce); } 

以下卡片示例利用了之前的 CSS 代码段。所有卡片(包括新添加的卡片)都通过一个选择器 html::view-transition-group(.card) 应用相同的计时。

卡片演示的录制内容。使用 view-transition-class 时,系统会将相同的 animation-timing-function 应用于所有卡,但添加或移除的卡除外。

调试过渡

由于视图过渡是基于 CSS 动画构建的,因此 Chrome 开发者工具中的动画面板非常适合调试过渡。

使用动画面板,您可以暂停下一个动画,然后前后拖动动画。在此期间,您可以在元素面板中找到过渡伪元素。

使用 Chrome 开发者工具调试视图过渡。

过渡元素不必是同一 DOM 元素

到目前为止,我们已使用 view-transition-name 为标题和标题中的文字创建了单独的过渡元素。从概念上讲,这些元素在 DOM 更改前后是相同的,但您可以创建并非如此的过渡效果。

例如,可以为主要视频嵌入代码指定 view-transition-name

.full-embed {  view-transition-name: full-embed; } 

然后,当点击缩略图时,可以为其提供相同的 view-transition-name,但仅在过渡期间有效:

thumbnail.onclick = async () => {  thumbnail.style.viewTransitionName = 'full-embed';  document.startViewTransition(() => {  thumbnail.style.viewTransitionName = '';  updateTheDOMSomehow();  }); }; 

结果:

一个元素过渡到另一个元素。极简演示来源

缩略图现在会过渡到主图片。虽然它们在概念上(以及实际上)是不同的元素,但过渡 API 会将它们视为同一事物,因为它们共享相同的 view-transition-name

此过渡的实际代码比上例略微复杂,因为它还处理向缩略图页面的过渡。如需查看完整实现,请参阅源代码


自定义进入和退出过渡效果

看看下面这个示例:

进入和退出边栏。极简演示来源

边栏是过渡的一部分:

.sidebar {  view-transition-name: sidebar; } 

不过,与上例中的标题不同,侧边栏不会显示在所有网页上。如果两种状态都有边栏,则过渡伪元素如下所示:

::view-transition ├─ …other transition groups… └─ ::view-transition-group(sidebar)   └─ ::view-transition-image-pair(sidebar)   ├─ ::view-transition-old(sidebar)   └─ ::view-transition-new(sidebar) 

不过,如果边栏仅位于新网页上,则不会有 ::view-transition-old(sidebar) 伪元素。由于侧边栏没有“旧”图片,因此图片对将仅包含 ::view-transition-new(sidebar)。同样,如果侧边栏仅位于旧网页上,则图片对将仅包含 ::view-transition-old(sidebar)

在之前的演示中,侧边栏的过渡效果因其是进入、退出还是同时存在于两种状态而异。它从右侧滑动并淡入,通过滑动到右侧并淡出,当它同时出现在两种状态时,会保持在原位。

如需创建特定的进入和退出过渡效果,您可以使用 :only-child 伪类来定位图片对中唯一的子元素(旧伪元素或新伪元素):

/* Entry transition */ ::view-transition-new(sidebar):only-child {  animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,  300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; } /* Exit transition */ ::view-transition-old(sidebar):only-child {  animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,  300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right; } 

在这种情况下,由于默认设置非常理想,因此当边栏在两种状态下都存在时,无需进行特定过渡。

异步 DOM 更新和等待内容

传递给 .startViewTransition() 的回调可以返回一个 promise,从而实现异步 DOM 更新,并等待重要内容准备就绪。

document.startViewTransition(async () => {  await something;  await updateTheDOMSomehow();  await somethingElse; }); 

在 Promise 实现之前,不会开始过渡。在此期间,网页处于冻结状态,因此应尽可能缩短延迟时间。具体而言,网络提取应在调用 .startViewTransition() 之前完成,此时页面仍处于完全互动状态,而不是在 .startViewTransition() 回调中完成。

如果您决定等待图片或字体准备就绪,请务必使用较长的超时时间:

const wait = ms => new Promise(r => setTimeout(r, ms)); document.startViewTransition(async () => {  updateTheDOMSomehow();  // Pause for up to 100ms for fonts to be ready:  await Promise.race([document.fonts.ready, wait(100)]); }); 

不过,在某些情况下,最好完全避免延迟,并使用您已有的内容。


充分利用您已有的内容

如果缩略图过渡到更大的图片:

缩略图过渡到更大的图片。试用演示网站

默认过渡效果是淡入淡出,这意味着缩略图可能会与尚未加载的完整图片淡入淡出。

一种处理方法是在开始过渡之前等待完整图片加载完毕。最好在调用 .startViewTransition() 之前完成此操作,这样网页会保持互动状态,并且可以显示微调器来向用户表明正在加载内容。不过,在这种情况下,还有一种更好的方法:

::view-transition-old(full-embed), ::view-transition-new(full-embed) {  /* Prevent the default animation,  so both views remain opacity:1 throughout the transition */  animation: none;  /* Use normal blending,  so the new view sits on top and obscures the old view */  mix-blend-mode: normal; } 

现在,缩略图不会淡出,而是会显示在完整图片下方。这意味着,如果新视图尚未加载,缩略图在整个过渡期间都会显示。这意味着过渡可以立即开始,完整图片可以自行加载。

如果新视图具有透明度,则此方法将不起作用,但在这种情况下,我们知道新视图不具有透明度,因此可以进行此优化。

处理宽高比变化

到目前为止,所有转换都是针对具有相同宽高比的元素,但情况并不总是如此。如果缩略图的宽高比为 1:1,而主图片的宽高比为 16:9,会怎么样?

一个元素过渡到另一个元素,并发生宽高比变化。极简演示来源

在默认过渡中,群组会从之前的尺寸动画过渡到之后的尺寸。旧视图和新视图的宽度均为组的 100%,高度为自动,这意味着无论组的大小如何,它们都会保持宽高比。

这是一个不错的默认值,但在此示例中,我们并不需要此值。因此:

::view-transition-old(full-embed), ::view-transition-new(full-embed) {  /* Prevent the default animation,  so both views remain opacity:1 throughout the transition */  animation: none;  /* Use normal blending,  so the new view sits on top and obscures the old view */  mix-blend-mode: normal;  /* Make the height the same as the group,  meaning the view size might not match its aspect-ratio. */  height: 100%;  /* Clip any overflow of the view */  overflow: clip; } /* The old view is the thumbnail */ ::view-transition-old(full-embed) {  /* Maintain the aspect ratio of the view,  by shrinking it to fit within the bounds of the element */  object-fit: contain; } /* The new view is the full image */ ::view-transition-new(full-embed) {  /* Maintain the aspect ratio of the view,  by growing it to cover the bounds of the element */  object-fit: cover; } 

这意味着,随着宽度扩大,缩略图会保持在元素中心,但完整图片会随着宽高比从 1:1 变为 16:9 而“取消裁剪”。

如需了解详情,请参阅视图过渡:处理宽高比变化


使用媒体查询来更改不同设备状态下的过渡效果

您可能希望在移动设备和桌面设备上使用不同的过渡效果,例如,以下示例在移动设备上执行从侧边滑入的完整滑动,但在桌面设备上执行更细微的滑动:

一个元素过渡到另一个元素。极简演示来源

这可以通过使用常规媒体查询来实现:

/* Transitions for mobile */ ::view-transition-old(root) {  animation: 300ms ease-out both full-slide-to-left; } ::view-transition-new(root) {  animation: 300ms ease-out both full-slide-from-right; } @media (min-width: 500px) {  /* Overrides for larger displays.  This is the shared axis transition from earlier in the article. */  ::view-transition-old(root) {  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,  300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;  }  ::view-transition-new(root) {  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,  300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;  } } 

您可能还需要根据匹配的媒体查询来更改分配 view-transition-name 的元素。


对“减少动画效果”偏好设置做出反应

用户可以通过操作系统表明自己偏好减少动画效果,并且该偏好会在 CSS 中公开

您可以选择阻止这些用户进行任何转换:

@media (prefers-reduced-motion) {  ::view-transition-group(*),  ::view-transition-old(*),  ::view-transition-new(*) {  animation: none !important;  } } 

不过,偏好“减少动画效果”并不意味着用户希望不显示动画效果。您可以选择更细致的动画,但仍能表达元素之间的关系和数据流,而不是使用上述代码段。


使用视图过渡类型处理多种视图过渡样式

Browser Support

  • Chrome: 125.
  • Edge: 125.
  • Firefox Technology Preview: supported.
  • Safari: 18.

Source

有时,从一个特定视图到另一个视图的过渡应具有专门定制的过渡效果。例如,在分页序列中前往下一页或上一页时,您可能希望根据您前往的是序列中编号较高的页面还是编号较低的页面,以不同的方向滑动内容。

分页演示的录制视频。
它会根据您要前往的页面使用不同的过渡效果。

为此,您可以使用视图过渡类型,以便为有效的视图过渡分配一种或多种类型。例如,在分页序列中过渡到较高页时使用 forwards 类型,而在过渡到较低页时使用 backwards 类型。这些类型仅在捕获或执行过渡时处于有效状态,并且每种类型都可以通过 CSS 进行自定义,以使用不同的动画。

如需在同一文档视图过渡中使用类型,请将 types 传递到 startViewTransition 方法中。为了实现这一点,document.startViewTransition 还接受一个对象:update 是用于更新 DOM 的回调函数,types 是包含类型的数组。

const direction = determineBackwardsOrForwards(); const t = document.startViewTransition({  update: updateTheDOMSomehow,  types: ['slide', direction], }); 

如需响应这些类型,请使用 :active-view-transition-type() 选择器。将要定位的 type 传递到选择器中。这样一来,您就可以将多个视图过渡的样式彼此分开,而不会出现一个视图过渡的声明干扰另一个视图过渡的声明的情况。

由于类型仅在捕获或执行过渡时应用,因此您可以使用选择器仅针对具有该类型的视图过渡在元素上设置或取消设置 view-transition-name

/* Determine what gets captured when the type is forwards or backwards */ html:active-view-transition-type(forwards, backwards) {  :root {  view-transition-name: none;  }  article {  view-transition-name: content;  }  .pagination {  view-transition-name: pagination;  } } /* Animation styles for forwards type only */ html:active-view-transition-type(forwards) {  &::view-transition-old(content) {  animation-name: slide-out-to-left;  }  &::view-transition-new(content) {  animation-name: slide-in-from-right;  } } /* Animation styles for backwards type only */ html:active-view-transition-type(backwards) {  &::view-transition-old(content) {  animation-name: slide-out-to-right;  }  &::view-transition-new(content) {  animation-name: slide-in-from-left;  } } /* Animation styles for reload type only (using the default root snapshot) */ html:active-view-transition-type(reload) {  &::view-transition-old(root) {  animation-name: fade-out, scale-down;  }  &::view-transition-new(root) {  animation-delay: 0.25s;  animation-name: fade-in, scale-up;  } } 

在下面的分页演示中,页面内容会根据您前往的页码向前或向后滑动。类型是在点击时确定的,然后传递到 document.startViewTransition 中。

如需定位任何有效的视图过渡(无论类型如何),您可以使用 :active-view-transition 伪类选择器。

html:active-view-transition {   } 

通过视图转换根上的类名称处理多种视图转换样式

有时,从一种特定类型的视图到另一种视图的过渡应具有专门定制的过渡效果。或者,“返回”导航应不同于“前进”导航。

返回时采用不同的过渡效果。极简演示来源

在引入过渡类型之前,处理这些情况的方法是在过渡根上临时设置一个类名。调用 document.startViewTransition 时,此过渡根是 <html> 元素,可以使用 JavaScript 中的 document.documentElement 进行访问:

if (isBackNavigation) {  document.documentElement.classList.add('back-transition'); } const transition = document.startViewTransition(() =>  updateTheDOMSomehow(data) ); try {  await transition.finished; } finally {  document.documentElement.classList.remove('back-transition'); } 

为了在过渡完成后移除类,此示例使用了 transition.finished,这是一个在过渡达到最终状态后即会解析的 promise。此对象的其他属性将在 API 参考文档中介绍。

现在,您可以在 CSS 中使用该类名称来更改过渡效果:

/* 'Forward' transitions */ ::view-transition-old(root) {  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,  300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; } ::view-transition-new(root) {  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms  cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; } /* Overrides for 'back' transitions */ .back-transition::view-transition-old(root) {  animation-name: fade-out, slide-to-right; } .back-transition::view-transition-new(root) {  animation-name: fade-in, slide-from-left; } 

与媒体查询一样,这些类的存在也可用于更改哪些元素获得 view-transition-name


运行过渡效果,而不会冻结其他动画

请观看以下视频,了解视频位置转换的演示:

视频过渡效果。极简演示来源

您发现有什么问题吗?如果您没有这样做,也不必担心。以下是慢速播放的视频:

视频转场效果,速度较慢。极简演示来源

在过渡期间,视频似乎会冻结,然后播放版本会淡入。这是因为 ::view-transition-old(video) 是旧视图的屏幕截图,而 ::view-transition-new(video) 是新视图的实时图片。

您可以解决此问题,但首先要问自己是否值得解决。如果您在转场以正常速度播放时没有发现“问题”,则无需更改。

如果您确实想修复此问题,请不要显示 ::view-transition-old(video),直接切换到 ::view-transition-new(video)。您可以通过替换默认样式和动画来实现此目的:

::view-transition-old(video) {  /* Don't show the frozen old view */  display: none; } ::view-transition-new(video) {  /* Don't fade the new view in */  animation: none; } 

这样就大功告成了!

视频转场效果,速度较慢。极简演示来源

现在,视频会在整个过渡期间播放。


与 Navigation API(和其他框架)集成

视图转换的指定方式使其可以与其他框架或库集成。例如,如果您的单页应用 (SPA) 使用路由器,您可以调整路由器的更新机制,以使用视图转换来更新内容。

在以下代码段(摘自此分页演示)中,Navigation API 的拦截处理程序经过调整,可在支持视图过渡时调用 document.startViewTransition

navigation.addEventListener("navigate", (e) => {  // Don't intercept if not needed  if (shouldNotIntercept(e)) return;  // Intercept the navigation  e.intercept({  handler: async () => {  // Fetch the new content  const newContent = await fetchNewContent(e.destination.url, {  signal: e.signal,  });  // The UA does not support View Transitions, or the UA  // already provided a Visual Transition by itself (e.g. swipe back).  // In either case, update the DOM directly  if (!document.startViewTransition || e.hasUAVisualTransition) {  setContent(newContent);  return;  }  // Update the content using a View Transition  const t = document.startViewTransition(() => {  setContent(newContent);  });  }  }); }); 

当用户通过滑动手势进行导航时,部分(但并非全部)浏览器会提供自己的转场效果。在这种情况下,您不应触发自己的视图过渡,因为这会导致用户体验不佳或令人困惑。用户会看到两个依次运行的过渡效果,一个由浏览器提供,另一个由您提供。

因此,建议防止在浏览器提供自己的视觉过渡效果时启动视图过渡。为此,请检查 NavigateEvent 实例的hasUAVisualTransition 属性的值。如果浏览器提供了视觉过渡效果,则该属性会设置为 true。此 hasUIVisualTransition 属性也存在于 PopStateEvent 实例中。

在前面的代码段中,用于确定是否运行视图过渡的检查会考虑此属性。如果不支持同文档视图转换,或者浏览器已提供自己的转换,则会跳过视图转换。

if (!document.startViewTransition || e.hasUAVisualTransition) {  setContent(newContent);  return; } 

在以下录制内容中,用户通过滑动操作返回到上一页。左侧的捕获不包含对 hasUAVisualTransition 标志的检查。右侧的录制内容包含检查,因此跳过了手动视图过渡,因为浏览器提供了视觉过渡。

同一网站在不检查 hasUAVisualTransition(左)和检查 hasUAVisualTransition(右)时的比较

使用 JavaScript 实现动画效果

到目前为止,所有过渡效果都是使用 CSS 定义的,但有时 CSS 并不够用:

圆形过渡。极简演示来源

此过渡效果的几个部分无法仅通过 CSS 实现:

  • 动画从点击位置开始播放。
  • 动画结束时,圆形的半径会达到最远角。不过,希望未来能通过 CSS 实现此目的。

幸运的是,您可以使用 Web Animation API 创建过渡效果!

let lastClick; addEventListener('click', event => (lastClick = event)); function spaNavigate(data) {  // Fallback for browsers that don't support this API:  if (!document.startViewTransition) {  updateTheDOMSomehow(data);  return;  }  // Get the click position, or fallback to the middle of the screen  const x = lastClick?.clientX ?? innerWidth / 2;  const y = lastClick?.clientY ?? innerHeight / 2;  // Get the distance to the furthest corner  const endRadius = Math.hypot(  Math.max(x, innerWidth - x),  Math.max(y, innerHeight - y)  );  // With a transition:  const transition = document.startViewTransition(() => {  updateTheDOMSomehow(data);  });  // Wait for the pseudo-elements to be created:  transition.ready.then(() => {  // Animate the root's new view  document.documentElement.animate(  {  clipPath: [  `circle(0 at ${x}px ${y}px)`,  `circle(${endRadius}px at ${x}px ${y}px)`,  ],  },  {  duration: 500,  easing: 'ease-in',  // Specify which pseudo-element to animate  pseudoElement: '::view-transition-new(root)',  }  );  }); } 

此示例使用了 transition.ready,这是一个在成功创建过渡伪元素后即会解析的 promise。此对象的其他属性将在 API 参考文档中介绍。


将过渡作为增强功能

View Transition API 旨在“封装”DOM 更改并为其创建过渡效果。不过,过渡应被视为一种增强功能,也就是说,如果 DOM 更改成功,但过渡失败,您的应用不应进入“错误”状态。理想情况下,过渡不应失败,但如果失败,也不应破坏其余用户体验。

为了将过渡视为增强功能,请注意不要以会导致应用在过渡失败时抛出异常的方式使用过渡 promise。

错误做法
async function switchView(data) {  // Fallback for browsers that don't support this API:  if (!document.startViewTransition) {  await updateTheDOM(data);  return;  }  const transition = document.startViewTransition(async () => {  await updateTheDOM(data);  });  await transition.ready;  document.documentElement.animate(  {  clipPath: [`inset(50%)`, `inset(0)`],  },  {  duration: 500,  easing: 'ease-in',  pseudoElement: '::view-transition-new(root)',  }  ); }

此示例的问题在于,如果转换无法达到 ready 状态,switchView() 将会拒绝,但这并不意味着视图切换失败。DOM 可能已成功更新,但存在重复的 view-transition-name,因此跳过了过渡。

相反:

正确做法
async function switchView(data) {  // Fallback for browsers that don't support this API:  if (!document.startViewTransition) {  await updateTheDOM(data);  return;  }  const transition = document.startViewTransition(async () => {  await updateTheDOM(data);  });  animateFromMiddle(transition);  await transition.updateCallbackDone; } async function animateFromMiddle(transition) {  try {  await transition.ready;  document.documentElement.animate(  {  clipPath: [`inset(50%)`, `inset(0)`],  },  {  duration: 500,  easing: 'ease-in',  pseudoElement: '::view-transition-new(root)',  }  );  } catch (err) {  // You might want to log this error, but it shouldn't break the app  } }

此示例使用 transition.updateCallbackDone 等待 DOM 更新,并在更新失败时拒绝。switchView 不再在转换失败时拒绝,而是在 DOM 更新完成时解析,并在失败时拒绝。

如果您希望 switchView 在新视图“稳定”时(即任何动画过渡已完成或已跳到结尾)解析,请将 transition.updateCallbackDone 替换为 transition.finished


不是填充,但…

此功能不易进行 Polyfill。不过,此辅助函数可在不支持视图过渡的浏览器中简化操作:

function transitionHelper({  skipTransition = false,  types = [],  update, }) {  const unsupported = (error) => {  const updateCallbackDone = Promise.resolve(update()).then(() => {});  return {  ready: Promise.reject(Error(error)),  updateCallbackDone,  finished: updateCallbackDone,  skipTransition: () => {},  types,  };  }  if (skipTransition || !document.startViewTransition) {  return unsupported('View Transitions are not supported in this browser');  }  try {  const transition = document.startViewTransition({  update,  types,  });  return transition;  } catch (e) {  return unsupported('View Transitions with types are not supported in this browser');  } } 

并且可以像这样使用:

function spaNavigate(data) {  const types = isBackNavigation ? ['back-transition'] : [];  const transition = transitionHelper({  update() {  updateTheDOMSomehow(data);  },  types,  });  // … } 

在不支持视图转换的浏览器中,系统仍会调用 updateDOM,但不会有动画转换。

您还可以提供一些 classNames 以在过渡期间添加到 <html>,从而更轻松地根据导航类型更改过渡

即使在支持视图转换的浏览器中,如果您不想要动画,也可以将 true 传递给 skipTransition。如果您的网站具有用于停用过渡效果的用户偏好设置,此属性会非常有用。


使用框架

如果您使用的是可抽象出 DOM 更改的库或框架,那么棘手之处在于如何知道 DOM 更改何时完成。以下是一组示例,使用了上述 辅助函数,适用于各种框架。

  • React - 此处的关键是 flushSync,它会同步应用一组状态更改。没错,使用该 API 时会显示一条很大的警告,但 Dan Abramov 向我保证,在这种情况下使用该 API 是合适的。与 React 和异步代码一样,在使用 startViewTransition 返回的各种 Promise 时,请注意确保代码以正确的状态运行。
  • Vue.js - 此处,关键是 nextTick,它会在 DOM 更新后完成。
  • Svelte - 与 Vue 非常相似,但等待下一次更改的方法是 tick
  • Lit - 这里的关键是组件中的 this.updateComplete promise,它会在 DOM 更新后兑现。
  • Angular - 这里的关键是 applicationRef.tick,它会刷新待处理的 DOM 更改。从 Angular 版本 17 开始,您可以使用 @angular/router 随附的 withViewTransitions

API 参考文档

const viewTransition = document.startViewTransition(update)

开始新的 ViewTransition

update 是一个函数,会在捕获文档的当前状态后调用。

然后,当 updateCallback 返回的 promise 实现时,过渡会在下一帧开始。如果 updateCallback 返回的 promise 被拒绝,则转换会被放弃。

const viewTransition = document.startViewTransition({ update, types })

使用指定的类型启动新的 ViewTransition

在捕获文档的当前状态后,系统会调用 update

types 用于在捕获或执行过渡时设置过渡的活动类型。最初为空。如需了解详情,请参阅下文中的 viewTransition.types

ViewTransition 的实例成员:

viewTransition.updateCallbackDone

updateCallback 返回的 promise 完成时,该 promise 会完成;当该 promise 拒绝时,该 promise 会拒绝。

View Transition API 会封装 DOM 更改并创建过渡效果。不过,有时您可能并不关心过渡动画的成功或失败,而只是想知道 DOM 何时发生变化。updateCallbackDone 适用于该用例。

viewTransition.ready

一个 Promise,在创建过渡的伪元素且动画即将开始时兑现。

如果过渡无法开始,则会拒绝。这可能是由于配置错误(例如重复的 view-transition-name)或 updateCallback 返回被拒绝的 promise 所致。

这对于使用 JavaScript 为过渡伪元素添加动画效果很有用。

viewTransition.finished

一种承诺,在最终状态完全可见且可供用户互动时兑现。

只有当 updateCallback 返回被拒绝的 promise 时,它才会拒绝,因为这表示未创建结束状态。

否则,如果转换未能开始或在转换期间被跳过,仍会达到结束状态,因此 finished 会实现。

viewTransition.types

一个类似于 Set 的对象,用于保存活跃视图过渡的类型。如需操纵条目,请使用 其实例方法 clear()add()delete()

如需在 CSS 中响应特定类型,请在过渡根上使用 :active-view-transition-type(type) 伪类选择器。

视图过渡完成时,类型会自动清理。

viewTransition.skipTransition()

跳过过渡的动画部分。

这不会跳过调用 updateCallback,因为 DOM 更改与过渡是分开的。


默认样式和过渡效果参考

::view-transition
填充视口的根伪元素,包含每个 ::view-transition-group
::view-transition-group

绝对定位。

“之前”状态和“之后”状态之间的过渡 widthheight

在“之前”和“之后”视口空间四边形之间进行过渡 transform

::view-transition-image-pair

绝对定位以填充组。

具有 isolation: isolate 以限制 mix-blend-mode 对旧视图和新视图的影响。

::view-transition-new::view-transition-old

绝对定位到封装容器的左上角。

填充组宽度的 100%,但高度为自动,因此会保持宽高比,而不是填充整个组。

具有 mix-blend-mode: plus-lighter 以实现真正的淡入淡出效果。

旧版视图从 opacity: 1 过渡到 opacity: 0。新视图从 opacity: 0 过渡到 opacity: 1


反馈

我们随时欢迎开发者的反馈。为此,请在 GitHub 上向 CSS 工作组提交问题,并附上建议和问题。在问题前添加 [css-view-transitions] 前缀。

如果您遇到 bug,请改为提交 Chromium bug