一、它是什么?
你用过手机上的 App 吧?从一个页面滑到另一个页面,图片从缩略图放大成全屏,导航栏稳稳地待在那儿不动——一切连贯流畅,你的眼睛不需要重新"找"东西在哪里。
但在网页上呢?点击一个链接,白屏一闪,新内容突然就出现了。就像翻漫画——啪一下,下一页突然就在眼前。你的大脑得重新定位:"标题在哪?导航在哪?"
View Transition API 就是来解决这个问题的。
它让浏览器在视图切换时自动创建平滑的过渡动画——旧的东西淡出,新的东西淡入,特定元素甚至可以从旧位置滑到新位置。一句话:让网页的状态切换有"电影感",而不是"PPT感"。
底层原理
先在大脑里播放一下这个"电影"是怎么拍出来的:
- 浏览器拍一张旧界面的快照(就像拍照片)
- 执行你的 DOM 更新(界面真的变了)
- 浏览器再拍一张新界面的快照
- 两张快照叠在一起,用 CSS 动画让旧淡出、新淡入
- 动画结束,扔掉快照,你看到的就是真实的新界面
没有魔法,就是两张快照叠在一起做动画。理解了这个物理图像,后面所有的 CSS 伪元素和 JavaScript API 都只是对这个过程的控制点。
二、为什么用它?
过去在 SPA 里做视图切换动画,你得自己:管理新旧内容并存、定位、写动画代码、清理旧 DOM、处理无障碍问题(屏幕阅读器困惑、焦点丢失、阅读位置跳)。几百行代码,容易出 bug。而 MPA 跨页面过渡——以前根本做不到,页面一导航旧页面就没了。
| 过去 | 现在 |
|---|---|
| 自己管理新旧内容并存 | 浏览器自动拍快照,新旧状态天然并存 |
| 自己写动画代码 | 默认交叉淡入淡出,零代码即可生效 |
| MPA 跨页面过渡不可能 | 一行 CSS 就能启用 |
| 无障碍问题要自己处理 | API 内置处理,避免焦点混乱 |
| 动画期间用户误触旧内容 | 旧快照是静态图像,不会误触 |
它不是给你一个"更强大的动画库"——而是把过渡动画的基础设施交给浏览器。你只需要说"我要过渡",其余的浏览器搞定。
但它不是万能的:核心是基于快照的 2D 过渡,适合元素滑移、缩放、淡入淡出这类效果。如果你需要 3D 变换、粒子效果、物理模拟,它不是最好的工具。
三、怎么用?
SPA 过渡(同文档)—— 5 分钟上手
function switchView() {
// 特性检测:不支持就正常更新,只是没动画
if (!document.startViewTransition) {
updateTheDOMSomehow();
return;
}
// 用 View Transition API 包裹 DOM 更新
document.startViewTransition(() => updateTheDOMSomehow());
}
把 DOM 更新包在 startViewTransition() 回调里。浏览器会拍旧快照、执行回调、拍新快照、自动做交叉淡入淡出。零 CSS。
特性检测那行不是装饰——它确保不支持 API 的浏览器仍正常工作。这是渐进增强,不是功能依赖。
MPA 过渡(跨文档)—— 一行 CSS
@view-transition {
navigation: auto;
}
两个页面(源和目标)都加这行,同源导航时自动过渡。零 JS,零 DOM 操作。 目前仅限同源页面。
自定义动画速度
::view-transition-group(root) {
animation-duration: 0.5s;
}
推荐在 ::view-transition-group() 上设置而非 old/new,因为 group 样式会被继承,避免层级间时长不一致。
给特定元素独立动画
默认整页一起淡入淡出。但很多时候你不想这样——导航栏应该不动,只有内容区域切换;图片应该从缩略图位置滑到全屏位置。
用 view-transition-name 把元素分离出来:
figcaption {
view-transition-name: figure-caption;
}
伪元素树变成:
::view-transition
├─ ::view-transition-group(root) ← 页面其余部分
│ └─ ::view-transition-image-pair(root)
│ ├─ ::view-transition-old(root)
│ └─ ::view-transition-new(root)
└─ ::view-transition-group(figure-caption) ← figcaption 独立动画
└─ ::view-transition-image-pair(figure-caption)
├─ ::view-transition-old(figure-caption)
└─ ::view-transition-new(figure-caption)
然后分别设置动画:
::view-transition-old(figure-caption) {
animation: shrink-x 0.25s ease-in forwards;
}
::view-transition-new(figure-caption) {
animation: grow-x 0.25s ease-out forwards;
}
@keyframes shrink-x { from { transform: scaleX(1); } to { transform: scaleX(0); } }
@keyframes grow-x { from { transform: scaleX(0); } to { transform: scaleX(1); } }
更聪明的做法:如果你只是想让元素从旧尺寸过渡到新尺寸,不用自己写动画——浏览器默认就会对 width/height 做平滑缩放,设个固定高度即可:
figcaption {
view-transition-name: figure-caption;
height: 30px;
}
各伪元素的作用:
| 伪元素 | 是什么 | 你能做什么 |
|---|---|---|
::view-transition |
覆盖层根,盖在页面内容之上 | 设置背景色等 |
::view-transition-group() |
快照容器 | 控制位置、尺寸、时长 |
::view-transition-image-pair() |
旧/新快照容器 | 控制混合模式 |
::view-transition-old() |
旧视图静态快照 | 自定义淡出动画 |
::view-transition-new() |
新视图实时快照(可交互) | 自定义淡入动画 |
高级:圆形揭示动画
原理一样,只是把默认淡入淡出换成从点击位置扩散的圆形揭示。关掉默认动画,用 Web Animations API:
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-old(root) { z-index: 1; }
::view-transition-new(root) { z-index: 9999; }
document.startViewTransition(() => updateDOM()).ready.then(() => {
const x = event.clientX;
const y = event.clientY;
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y)
);
document.documentElement.animate(
{
clipPath: [
`circle(0px at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
],
},
{
duration: 500,
easing: 'ease-in-out',
pseudoElement: '::view-transition-new(root)',
}
);
});
关键:.ready Promise 完成时伪元素树已建好,是 JS 插手改动画的时机。用 clipPath 从小圆变大圆——像从点击位置炸开。
ViewTransition 对象生命周期
startViewTransition() 返回的对象有三个 Promise:
| Promise | 完成时机 | 用途 |
|---|---|---|
updateCallbackDone |
DOM 更新回调完毕 | 确认 DOM 已更新 |
ready |
伪元素树已建好,动画即将开始 | 用 Web Animations API 替换默认动画 |
finished |
动画结束,新界面完全可见 | 清理临时状态 |
还有 skipTransition() 可中途取消动画。
MPA 进阶:pageswap / pagereveal 事件
跨文档过渡没有 JS 触发 API,但可通过两个事件插手——出站页面用 pageswap,入站页面用 pagereveal,分别给对应元素设置 view-transition-name 来实现"共享元素"过渡:
// 出站页面:给被点击的链接设名字
window.addEventListener('pageswap', (event) => {
if (!event.viewTransition) return;
const clickedLink = document.querySelector(`a[href="${toURL.pathname}"]`);
if (clickedLink) clickedLink.style.viewTransitionName = 'profile-image';
// 快照拍完后必须清除!
event.viewTransition.finished.then(() => {
if (clickedLink) clickedLink.style.viewTransitionName = '';
});
});
// 入站页面:给目标元素设同样的名字
window.addEventListener('pagereveal', (event) => {
if (!event.viewTransition) return;
const img = document.querySelector('.profile-image');
if (img) img.style.viewTransitionName = 'profile-image';
event.viewTransition.finished.then(() => {
if (img) img.style.viewTransitionName = '';
});
});
踩坑提醒:快照拍完后必须清除 view-transition-name。不清除的话,名字会留在 bfcache 中,用户按返回按钮时 pagereveal 可能给另一个元素设了同样的名字——两个元素同名,过渡直接被跳过。
四、兼容性
SPA 同文档过渡 —— Baseline Newly Available
2025 年 10 月 14 日,随着 Firefox 144 发布,同文档视图过渡在三大引擎全部支持。
| 浏览器 | 最低版本 | 状态 |
|---|---|---|
| Chrome / Edge | 111 | ✅ |
| Safari | 18 | ✅ |
| Firefox | 144 | ✅ |
| Samsung Internet | 22 | ✅ |
加上特性检测可安全使用。注意 Firefox 144 暂不支持 view transition types。
MPA 跨文档过渡 —— 仍在演进
| 浏览器 | 最低版本 | 状态 |
|---|---|---|
| Chrome / Edge | 126 | ✅ |
| Safari | 18.2 | ✅ |
| Firefox | — | ❌ 不支持 |
Firefox 尚未实现跨文档过渡,是 Interop 2025 推进目标之一。
各子特性速查
| 特性 | Chrome | Safari | Firefox |
|---|---|---|---|
startViewTransition() |
111+ | 18+ | 144+ |
@view-transition 规则 |
126+ | 18.2+ | ❌ |
view-transition-name |
111+ | 18+ | 144+ |
view-transition-class |
125+ | 18.2+ | 144+ |
:active-view-transition |
125+ | 18.2+ | 144+ |
pageswap / pagereveal
|
126+ | 18.2+ | ❌ |
element.startViewTransition() |
128+ | ❌ | ❌ |
实际建议
- SPA 项目:放心用,三大浏览器全支持,降级路径清晰
- MPA 项目:Chrome/Safari 可用,Firefox 不支持。可在 MPA 中混合使用——页面内状态变化用 SPA 方式,页面间导航等 Firefox 支持后再启用
- 核心原则:渐进增强。不支持就正常工作没动画,你大胆用,出错只是回到"没动画"状态
总结
回到那个物理图像:浏览器拍两张快照,叠在一起做动画。
所有功能都是对这个过程的控制点:
-
document.startViewTransition()→ 告诉浏览器"开始拍" -
view-transition-name→ 告诉浏览器"这个元素单独拍" -
::view-transition-old/new()→ 控制两张快照怎么动 -
@view-transition { navigation: auto }→ 告诉浏览器"页面导航时自动拍"
它不是动画库,是基础设施级别的改变——就像 CSS 让你不再用 JS 管样式一样,View Transition API 让你不再用 JS 管视图切换动画。最直接的解决方案,往往就是最好的。