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

发布时间:2025 年 9 月 22 日

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

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

  • 剪裁(overflow、clip-path、border-radius):剪裁会影响元素的子元素,这意味着视图过渡组同级元素无法相互剪裁。
  • opacity、mask-image 和 filter:同样,这些效果旨在作用于树的完全光栅化图片,影响子项,而不是单独影响每个项。
  • 3D 转换(transform-style、transform、perspective):为了显示全方位的 3D 转换动画,需要保持一定的层次结构。

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

视图过渡运行期间剪裁效果损坏的录制内容。文本应被对话框剪裁,但实际并未剪裁。动画时间已放慢,以夸大效果。

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

Browser Support

  • Chrome: 140.
  • Edge: 140.
  • 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-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-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)
  ├─ head
  └─ body
        └─ …

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

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

结果如下:

实时演示

试唱录制

试唱录制(降速版)

仅当使用嵌套组时,::view-transition-group-children 伪类才会存在。它的大小与原始元素的边框盒相同,并具有与生成伪元素的元素(上例中的 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 树的部分拓扑。这可以实现各种以前无法通过视图过渡实现的效果,其中一些效果已在本文中介绍。

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