跳转到内容

性能 · API

View Transitions API

发布于 更新于

document.startViewTransition(updateCallback) 把当前页面捕获为一组快照,运行改变 DOM 的回调,捕获新状态,再用 CSS 在两者之间做动画,默认是交叉淡入淡出。当两个页面都用 @view-transition 规则选入时,同一机制适用于跨文档导航,这让多页面 PWA 得到了过去需要客户端路由器才有的带动画的路由切换。

document.startViewTransition(updateCallback)
document.startViewTransition({ update, types })
transition.updateCallbackDone
transition.ready
transition.finished
transition.skipTransition()
@view-transition { navigation: auto; }

该方法同步返回一个 ViewTransition。同文档过渡存在于 Chrome 111、Safari 18 与 Firefox 144(BCD api.Document.startViewTransition);跨文档的 @view-transition 规则存在于 Chrome 126 和 Safari 18.2,Firefox 没有(BCD css.at-rules.view-transition)。

参数 类型 含义
updateCallback 返回值或 Promise 的函数 改变 DOM。旧快照在它运行前捕获,新快照在它返回的 Promise 落定后捕获;期间渲染暂停,所以要短。
options.update function 对象形式下与 updateCallback 相同。
options.types 字符串数组 Level 2:由 :active-view-transition-type() 匹配的过渡类型,让一份样式表能区别地处理「前进」与「后退」。
transition.updateCallbackDone Promise<void> 回调的 Promise 落定时落定。
transition.ready Promise<void> 伪元素已存在、动画即将开始时落定;是附加 Web Animations API 动画的时机。过渡被跳过时拒绝。
transition.finished Promise<void> 动画结束、伪元素移除之后落定。

动画运行在以 ::view-transition 为根的伪元素树上:每个 view-transition-name(默认的 root 加上 CSS 中被赋名的每个元素)对应一个 ::view-transition-group(name),其中含 ::view-transition-image-pair(name),后者包含 ::view-transition-old(name)(旧状态的位图)与 ::view-transition-new(name)(新状态的实时视图)。group 在两个状态之间对位置和尺寸做动画,old 与 new 图像交叉淡化,这一切都用普通的 CSS 动画重新定义样式。

异常 触发条件
InvalidStateError DOMException 捕获时两个元素共用同一个 view-transition-name。过渡被跳过,ready 拒绝,DOM 更新仍然生效。
TimeoutError DOMException 更新回调的 Promise 超过引擎限制(Chromium 为 4 s);过渡被跳过,更新生效。
AbortError DOMException 第一次过渡结束前又调用了 startViewTransition(),第一次被跳过;skipTransition() 以同样方式拒绝 ready。
updateCallback 抛出的任何错误 拒绝 updateCallbackDone、ready 与 finished;DOM 保持回调离开时的状态。

过渡被跳过不等于更新失败:上述每种情况下回调的改动都会生效,这正是该 API 在特性检测之后可以无条件调用的原因。

单页示例为共享元素做动画;多页示例只需要 CSS;最后一个是守卫。

在单页应用中为列表到详情的导航做动画

Section titled “在单页应用中为列表到详情的导航做动画”

缩略图和详情页主图在过渡前被赋予同一个 view-transition-name,于是 group 把图片从一处动画到另一处,而不是整页交叉淡化。

async function openDetail(item) {
const thumb = document.querySelector(`[data-id="${item.id}"] img`);
thumb.style.viewTransitionName = 'hero';
const transition = document.startViewTransition(async () => {
await renderDetail(item); // 用详情视图替换列表
document.querySelector('.detail img').style.viewTransitionName = 'hero';
});
await transition.finished;
thumb.style.viewTransitionName = ''; // 下一次捕获时每个名字只能有一个元素
}

事后清除名字很重要:之后的过渡若发现两个 hero 元素,会以 InvalidStateError 被跳过。

::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 250ms;
}
::view-transition-group(hero) {
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}

根的交叉淡化与 hero group 分别定义样式;没有第一条规则,默认交叉淡化同样是 250 ms,第二条只改变 hero 的缓动。

离开和进入的页面都带该规则;浏览器在 pageswap 时捕获旧页面、在 pagereveal 时捕获新页面,同样的伪元素做动画。仅限同源导航。

@view-transition {
navigation: auto;
}
header {
view-transition-name: site-header;
}

给页眉命名让它在页面其余部分交叉淡化时保持不动;预渲染的下一页(见 Speculation Rules)让过渡无需等待网络就能开始。

没有该 API 的引擎中该方法不存在。兜底直接应用 DOM 改动,最终状态相同,只是没有动画。

function transitionTo(update) {
if (!('startViewTransition' in document)) {
return update(); // 没有 View Transitions:不带动画地更新
}
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
return update(); // 用户要求减少动态效果:跳过动画
}
return document.startViewTransition(update).finished;
}

返回 finished 让调用方可以等动画结束后再做事,例如聚焦新视图的标题。

规范

规范状态
无。