你以为 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——移动端的体验会立刻上一个台阶。
评论区
登录后可评论。