使用嵌套的视图过渡组来防止视图过渡中的剪裁问题(以及其他问题)

发布时间:2025 年 9 月 22 日

当您启动视图过渡时,浏览器会自动拍摄标记为 view-transition-name 的元素在过渡前后的快照。这些快照会在伪元素的树中呈现。默认情况下,生成的树是“扁平”的。这意味着 DOM 中的原始层次结构会丢失,并且所有捕获的视图过渡组都将成为单个 ::view-transition 伪元素的同级元素。

这种扁平树方法足以满足许多用例,但有些样式用例无法通过这种方法实现。以下是一些可能会在扁平树中产生意外视觉效果的特效示例:

  • 剪裁(overflowclip-pathborder-radius):剪裁会影响元素的子元素,这意味着视图过渡组同级元素无法相互剪裁。
  • opacitymask-imagefilter:同样,这些效果旨在处理树的完全栅格化图像,影响子项,而不是单独影响每个项。
  • 3D 转换(transform-styletransformperspective):为了显示完整的 3D 转换动画,需要保持一定的层次结构。

以下示例展示了一个扁平的伪树,其中的元素被 DOM 树中的祖先剪裁。这些元素在视图过渡期间会失去剪裁效果,从而导致视觉效果损坏。

在视图转换运行时,录制了损坏的剪裁效果。文字应被对话框剪裁,但实际并未被剪裁。动画时间减慢,以夸大效果。

嵌套视图过渡组是视图过渡的扩展,可让您将 ::view-transition-group 伪元素相互嵌套。当视图过渡组嵌套时,可以在过渡期间恢复剪裁等效果。

Browser Support

  • Chrome: 140.
  • Edge: not supported.
  • Firefox: not supported.
  • Safari: not supported.

从扁平伪树到嵌套伪树

在以下演示中,您可以点击某人的头像来查看有关该人的更多信息。动画效果由同一文档视图过渡处理,该过渡会将点击的按钮变形为对话框,在屏幕上移动头像和名称,并使对话框中的段落向上或向下滑动。

实时演示

演示录制

演示录制(减速)

如果您仔细查看演示,就会发现过渡存在问题:即使包含说明的段落是 DOM 中 <dialog> 元素的子元素,在过渡期间,文本也不会被 <dialog> 的框剪裁:

<dialog id="info_bramus" closedby="any">
  <h2><img alt="…" class="avatar" height="96" width="96" src="avatar_bramus.jpg"> <span>Bramus</span></h2>
  <p>Bramus is …</p>
  <p>…</p>
</dialog>

<dialog> 上应用 overflow: clip 也不会执行任何操作。

问题在于视图过渡构建和渲染其伪树的方式:

  • 在伪树中,默认情况下,所有快照都是同级关系。
  • 伪树在 ::view-transition 伪元素中呈现,该伪元素呈现在整个文档的顶部。

对于此演示,DOM 树如下所示:

html
  ├─ ::view-transition
  │  ├─ ::view-transition-group(card)
  │  │  └─ ::view-transition-image-pair(card)
  │  │     ├─ ::view-transition-old(card)
  │  │     └─ ::view-transition-new(card)
  │  ├─ ::view-transition-group(name)
  │  │  └─ ::view-transition-image-pair(name)
  │  │     ├─ ::view-transition-old(name)
  │  │     └─ ::view-transition-new(name)
  │  ├─ ::view-transition-group(avatar)
  │  │  └─ ::view-transition-image-pair(avatar)
  │  │     ├─ ::view-transition-old(avatar)
  │  │     └─ ::view-transition-new(avatar)
  │  ├─ ::view-transition-group(paragraph1.text)
  │  │  └─ ::view-transition-image-pair(paragraph1.text)
  │  │     └─ ::view-transition-new(paragraph1.text)
  │  └─ ::view-transition-group(paragraph2.text)
  │     └─ ::view-transition-image-pair(paragraph2.text)
  │        └─ ::view-transition-new(paragraph2.text)
  ├─ head
  └─ body
        └─ …

由于 ::view-transition-group(.text) 伪元素是 ::view-transition-group(card) 伪元素的后续同级元素,因此它们会绘制在卡片顶部。

若要使 ::view-transition-group(card) 剪辑 ::view-transition-group(.text)::view-transition-group(.text) 伪元素应为 ::view-transition-group(card) 的子元素。为此,请使用 view-transition-group,该属性可让您为生成的 ::view-transition-group() 伪元素分配“父组”。

如需更改父群组,您可以采用以下两种方法:

  • 在父级上,将 view-transition-group 设置为 contain,使其包含所有具有 view-transition-name 的子级。
  • 在所有子级上,将 view-transition-group 设置为父级的 view-transition-name。您还可以使用 nearest 来定位最近的祖先群组。

因此,在此演示中,若要使用嵌套的视图过渡组,代码将变为:

button.clicked,
dialog {
  view-transition-group: contain;
}

button.clicked,
dialog *,
  view-transition-group: nearest;
}

添加此代码后,::view-transition-group(.text) 伪类现在会嵌套在 ::view-transition-group(card) 伪类中。这是通过额外的 ::view-transition-group-children(…) 伪类完成的,该伪类可将所有嵌套的伪类保持在一起:

html
  ├─ ::view-transition
  │  ├─ ::view-transition-group(card)
  │  │  ├─ ::view-transition-image-pair(card)
  │  │  │  ├─ ::view-transition-old(card)
  │  │  │  └─ ::view-transition-new(card)
  │  │  └─::view-transition-group-children(card)
  │  │    ├─ ::view-transition-group(paragraph1.text)
  │  │    │  └─ ::view-transition-image-pair(paragraph1.text)
  │  │    │     └─ ::view-transition-new(paragraph1.text)
  │  │    └─ ::view-transition-group(paragraph2.text)
  │  │       └─ ::view-transition-image-pair(paragraph2.text)
  │  │          └─ ::view-transition-new(paragraph2.text)
  │  ├─ ::view-transition-group(name)
  │  │  └─ ::view-transition-image-pair(name)
  │  │     ├─ ::view-transition-old(name)
  │  │     └─ ::view-transition-new(name)
  │  └─ ::view-transition-group(avatar)
  │     └─ ::view-transition-image-pair(avatar)
  │        ├─ ::view-transition-old(avatar)
  │        └─ ::view-transition-new(avatar)
  ├─ head
  └─ body
        └─ …

最后,为了让 ::view-transition-group(card) 伪元素剪裁段落,请将 overflow: clip 应用到 ::view-transition-group-children(card) 伪元素:

::view-transition-group-children(card) {
  overflow: clip;
}

结果如下:

实时演示

演示录制

演示录制(减速)

只有在使用嵌套组时,才会显示 ::view-transition-group-children 伪元素。它的大小与原始元素的 border-box 相同,并具有透明边框,其形状和边框粗细与生成伪元素的元素(即上一个示例中的 card)相同。

剪辑及更多功能

嵌套的视图过渡组可用于剪切效果以外的其他位置。另一个例子是 3D 效果。在以下演示中,有一个选项可在过渡期间以 3D 方式旋转卡片。

html:active-view-transition-type(open) {
    &::view-transition-old(card) {
        animation-name: rotate-out;
    }
    &::view-transition-new(card) {
        animation-name: rotate-in;
    }
}
html:active-view-transition-type(close) {
    &::view-transition-old(card) {
        animation-name: rotate-in;
    }
    &::view-transition-new(card) {
        animation-name: rotate-out;
    }
}

如果没有嵌套的视图过渡组,头像和名称不会随卡片一起旋转。

实时演示

演示录制

演示录制(减速)

通过将头像和名称伪元素嵌套在卡片中,可以恢复 3D 效果。但这并不是您需要做的唯一一件事。除了轮替 ::view-transition-old(card)::view-transition-new(card) 伪随机数之外,您还需要轮替 ::view-transition-group-children(card) 伪随机数。

html:active-view-transition-type(open) {
    &::view-transition-group-children(card) {
        animation: rotate-in var(--duration) ease;
        backface-visibility: hidden;
    }
}
html:active-view-transition-type(close) {
    &::view-transition-group-children(card) {
        animation: rotate-out var(--duration) ease;
        backface-visibility: hidden;
    }
}

实时演示

演示录制

演示录制(减速)

更多演示

在以下示例中,嵌套的视图过渡组用于确保卡片被其祖先滚动器剪裁。您可以使用随附的控件切换嵌套视图过渡组的使用。

实时演示

演示录制

此演示的有趣之处在于,所有 ::view-transition-group(.card) 伪元素都嵌套在祖先 ::view-transition-group(cards) 伪元素内,并被其剪裁。排除 #targeted-card,因为其进入/退出动画不应被 ::view-transition-group(cards) 剪裁。

/* The .cards wrapper contains all children */
.cards {
  view-transition-name: cards;
  view-transition-group: contain;
}

/* Contents that bleed out get clipped */
&::view-transition-group-children(cards) {
  overflow: clip;
}

/* Each card is given a v-t-name and v-t-class */
.card {
  view-transition-name: match-element;
  view-transition-class: card;
}

/* The targeted card is given a unique name (to style the pseudo differently)
   and shouldn't be contained by the ::view-transition-group-children(cards) pseudo */
#targeted-card {
  view-transition-name: targeted-card;
  view-transition-group: none;
}

回顾

嵌套视图过渡功能可在构建伪元素时保留 DOM 树的部分拓扑。这可实现之前无法通过视图过渡实现的各种效果,我们在此处介绍了一些效果。

嵌套会更改视图过渡的构建模型,旨在用于创建高级效果。如上所述,元素范围的视图过渡也可以通过更简单的模型实现部分效果。建议您同时试用这两项功能,以便确定哪项功能最符合您的需求。