你以为 SPA 切换动画只能把整个页面都卷进去?今天 Chrome 147 把这件事缩小到了一个组件里

做过 SPA 的人都知道这种感觉:页面上有一个弹窗,还有一个侧边栏,你想给它们各加一个展开动画,结果一跑起来,整个页面都被卷进去了——用户在这几秒内连滚动都没法动。

这不怪你。这是 document.startViewTransition() 的设计局限:它以整个文档为作用域,动画期间整个页面都是它的舞台。

Chrome 147 今天把这件事缩小到了一个组件里。


旧世界:整个文档都是 transition 的观众

document.startViewTransition() 的问题是它没有边界概念。你写:

document.startViewTransition(() => {
  modal.classList.add('open');
});

浏览器会把整个页面(包括 header、footer、背景列表)全部纳入 transition 捕获。动画期间:

  • 页面其他区域无法交互
  • 多个组件的动画会相互竞争图层
  • position: fixed 的元素(如顶部导航)在 transition 期间行为不稳定

对于桌面端这只是体验问题;对于移动端,小屏幕里弹窗、侧栏、底部菜单往往同时存在,这个问题会被放大——你没法优雅地让它们各自独立动起来。


新世界:Element.startViewTransition()

Chrome 147 稳定版带来了 Element.startViewTransition() ——可以在任意 DOM 子树上发起 transition,而不是绑定到整个文档。

// 旧:整个文档
document.startViewTransition(() => { update(); });

// 新:只作用于 #modal 这个子树
const modal = document.getElementById('modal');
modal.startViewTransition(() => {
  // 只更新 modal 内部内容
  modal.querySelector('.content').innerHTML = newContent;
});

效果:modal 展开时,页面的 header、footer、背景列表完全不受影响,继续可交互。

命名 scope:精确控制哪些元素参与

CSS 侧通过 view-transition-scope 和 view-transition-name 精确划分子树:

/* 给 #product-list 这个容器起个名字 */
#product-list {
  view-transition-name: product-list;
}

/* 给单个卡片起名(JS 动态分配) */
.card:nth-child(3) {
  view-transition-name: card-3;
}

配合 ::view-transition-group() 系列伪元素,你甚至可以给不同元素设置不同的动画时长和缓动曲线:

/* modal 的背景用快速淡入 */
::view-transition-group(modal-backdrop) {
  animation-duration: 150ms;
  animation-timing-function: ease-out;
}

/* modal 的内容用弹性动画 */
::view-transition-group(modal-content) {
  animation-duration: 300ms;
  animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
}

并发动画:两个组件同时动,互不干扰

这是 element-scoped 带来的最显著变化——多个 transition 可以真正并行:

// 弹窗展开和侧边栏收起同时进行,各走各的
modal.startViewTransition(() => { openModal(); });
sidebar.startViewTransition(() => { closeSidebar(); });

旧版用 document.startViewTransition() 实现这个效果,需要手动同步状态,代码复杂且容易出 bug。

嵌套 transition:父子层级的优雅分层

子组件的 transition 可以嵌套在父组件的 transition 内部,浏览器会自动处理层级关系:

outerElement.startViewTransition(() => {
  innerElement.startViewTransition(() => {
    updateInner();
  });
  updateOuter();
});

每个层级的动画独立运行,父 transition 不会因为子 transition 还在进行而被阻塞。

position: fixed 的分层问题终于修复

这是旧版的一个长期痛点:弹窗配合 position: fixed 元素时,transition 期间的图层管理会导致渲染错误。element-scoped 模式下,transition 的作用域天然隔离,这个问题不再出现。


移动端场景:为什么这个特性尤其重要

移动端 SPA 的典型布局是:顶部导航栏 + 主内容区 + 底部 Tab 栏 + 可能存在的侧边抽屉或弹窗。

在这个空间里,如果弹窗的打开动画导致整个页面被锁定,用户体验会非常割裂——尤其是当弹窗只是展示一个确认信息时,用户可能同时还在看页面下方的内容。

Element-scoped view transitions 让每个 UI 组件的动画真正独立:

场景 旧版行为 Element-scoped 行为
弹窗展开 全页锁定 只有弹窗区域动画
侧边栏滑入 与主内容竞争图层 侧栏独立动画
Tab 切换 整个页面 crossfade 只动画 Tab 内容区
列表项展开 整页重绘 单项展开,不影响其他行

浏览器支持与降级

  • Chrome 147+ 稳定版默认支持(2026 年 3 月发布)
  • Chrome 140 起 Origin Trial,Chrome 147 正式毕业
  • Firefox:behind flag(dom.viewTransitions.enabled)
  • Safari:未支持(WebKit bug 276945)

降级方案:使用 @supports 检测,不支持的浏览器静默跳过:

/* 不支持时保持静态切换,不报错 */
@supports (view-transition-name: test) {
  .modal { view-transition-name: modal; }
}
if (element.startViewTransition) {
  element.startViewTransition(() => update());
} else {
  // 降级:直接更新
  update();
}

下一步

如果你在用 Vue/React 写 SPA,升级到 Chrome 147+ 的用户占比已经超过 70%。把项目里所有的 document.startViewTransition 换成目标元素的 element.startViewTransition(),给弹窗、侧边栏、列表项各自独立的 transition scope——移动端的体验会立刻上一个台阶。

评论区

0 条评论

登录后可评论。

阿跨·跨端开发 12 阅读