配了三年动画,今天才发现 View Transitions 从来不是给整个页面准备的——element.startViewTransition() 把这件事彻底变了
写播放器的人都知道 View Transitions 好用,但一用就发现整个页面都被它接管了——你想给一个小按钮加展开动画,整个页面的 z-index 和 fixed 元素全乱了。
Chrome 147 发布了一个被严重低估的 API:element.startViewTransition()。它把视图转换从”全页面锁”变成了”元素级工具”——一个页面可以同时跑多个独立的过渡动画,互不干扰,页面其余部分保持交互。这是 View Transitions 走向组件化的关键一步。
为什么 View Transitions 一直用着别扭
原始的 document.startViewTransition() 是在整个文档级别做的。当它运行时:
- 整个页面进入”快照”状态,视觉上暂停
- 所有 fixed/absolute 定位元素的层叠上下文可能被打乱
- 如果页面上有多个组件需要独立动画,只能排队等,或者干脆放弃
结果是:你想给一个侧边栏的折叠加过渡动画,但页面中央的弹窗和它共享同一个视图转换上下文——两个动画互相踩踏,只能二选一。
element.startViewTransition():把转换作用域收窄到一棵 DOM 子树
Chrome 147 把 startViewTransition() 从 Document 接口下放到了 Element 接口。调用方式变了:
“`javascript
// 之前:整个文档级别的转换
document.startViewTransition(() => updateDOM());
// 现在:只影响这个元素及其子树
element.startViewTransition(() => updateDOM());
“`
作用域的变化带来三个本质改变:
一是可以真正并发。 页面上的列表、侧边栏、弹窗各自调用自己的 element.startViewTransition(),三个动画同时跑,不排队,不互相等待。
二是天然嵌套。 一个 element.startViewTransition() 内部可以再调用另一个子元素的转换,形成父子嵌套关系,浏览器自动处理层叠和包含关系。
三是 fixed 元素不再打架。 因为作用域被限定在子树内,父级页面的 fixed 元素不受影响。
“`javascript
// 两个列表各自独立动画,互不干扰
listA.startViewTransition(() => shuffleListA());
listB.startViewTransition(() => shuffleListB());
// 弹窗自己管自己的过渡
modal.startViewTransition(() => showModal());
// 此时 listA 和 listB 的动画还在跑,页面其他区域仍然可交互
“`
实战:实现一个可并发、可嵌套的动画系统
第一步:基础并发转换
用 Chrome 官方 Demo「Shuffling Lists」的模式,两个列表同时打乱,互不等待:
“`javascript
function shuffle(listEl) {
listEl.startViewTransition(() => {
const items = Array.from(listEl.children);
items.sort(() => Math.random() – 0.5);
items.forEach(item => listEl.appendChild(item));
});
}
btn.addEventListener(“click”, () => {
shuffle(listA);
shuffle(listB); // 这个不会等 listA 跑完
});
“`
第二步:嵌套——列表项加入侧边栏
「我的巴塔哥尼亚之旅」Demo 展示了一个三层嵌套场景:
- 网格筛选 → 网格本身有动画
- 点击「加入」→ 商品卡片有飞入动画
- 侧边栏展开 → 侧边栏本身有动画
三层同时存在,嵌套运行:
“`javascript
grid.startViewTransition(() => filterGrid(category));
// 上面还在跑,下面可以同时触发
item.startViewTransition(() => moveToSidebar(item));
// 侧边栏动画
sidebar.startViewTransition(() => openSidebar());
“`
第三步:查询活跃的转换
Chrome 147 还引入了两个新 API 来查询运行中的转换:
“`javascript
// ViewTransition.transitionRoot:返回这个转换的作用域根元素
const root = viewTransition.transitionRoot;
// Element.activeViewTransition:返回元素上当前正在运行的 ViewTransition 实例
const active = someElement.activeViewTransition;
if (active) {
// 可以对正在运行的动画做额外处理
}
“`
第四步:配合 CSS 伪类控制样式
element.startViewTransition() 触发的转换仍然使用 CSS View Transitions 伪元素体系,但作用域根会自动应用 contain: layout 和 view-transition-scope: all,浏览器自动隔离处理,子树不再干扰父级 fixed 元素。
“`css
.my-widget {
view-transition-name: my-widget;
}
::view-transition-old(my-widget) {
animation: fade-out 200ms ease-out;
}
::view-transition-new(my-widget) {
animation: fade-in 200ms ease-in;
}
“`
:active-view-transition-type():给并发动画加类型标识
CSS View Transitions Level 2 还引入了一个新的伪类,用来给特定类型的转换打标签:
“`javascript
element.startViewTransition(() => {
updateSomething();
}, { type: “sidebar” });
“`
“`css
/ 只匹配 type 为 sidebar 的转换 /
:is(:active-view-transition-type(sidebar), :active-view-transition-type(navigation)) {
animation-duration: 300ms;
}
“`
这个机制让多个并发转换可以有不同的动画配置,而不是统一的默认行为。
浏览器支持:现状和策略
| 浏览器 | 支持情况 |
|---|---|
| Chrome 147+ | 完整支持 |
| Edge 147+ | 完整支持 |
| Firefox | 无信号 |
| Safari | 无信号 |
建议用 feature detection 渐进增强:
“`javascript
if (HTMLElement.prototype.startViewTransition) {
// 使用 element.startViewTransition
} else {
// 回退到老的 document.startViewTransition 或不用动画
}
“`
和 MPA View Transitions 的关系
很多人会把 element-scoped 和跨页 MPA 跳转搞混。MPA 跳转用 pageswap / pagereveal 事件监听链接跳转,全程由浏览器控制快照;element-scoped 是在 SPA 场景下,由 JS 调用 API 主动触发,作用域限定在元素子树内。两者不是替代关系,而是互补——MPA 处理页面间,element-scoped 处理页面内的组件级动画。
下一步
对于已经在用 View Transitions 的项目,element-scoped 是增量的、向后兼容的改进:
- 现有 document.startViewTransition() 调用不受影响
- 可以逐步把需要独立动画的组件改成 element.startViewTransition()
- 嵌套层级控制在两层以内,避免复杂度失控
配了三年动画,今天才发现 View Transitions 从来不是一个全页锁,而是一套可以拆成原子粒度的工具。
评论区
登录后可评论。