页面一跳转就要等浏览器撕掉重画?这件事今天被两个 CSS 规则彻底变了
你做过一个电商详情页,卡片列表点进去——整个屏幕白一下,然后新页面硬生生铺上来。用户问:你们这是什么年代的交互?
根子不在代码,在浏览器切换文档的方式。你点一个链接,浏览器把旧页面直接扔掉,重建新页面的 DOM 和 CSSOM,这个过程没有任何过渡,叫”跨文档导航”。
2026 年,这件事被彻底变了。
两行 CSS,让跳转自己会动
Safari 18.2(2025 年 6 月 WWDC)和 Chrome 126(2025 年中)同时支持了跨文档 View Transitions,简称 MPA 场景的过渡动画。实现起来就两行:
/* page-a.html 和 page-b.html 都要写 */
@view-transition {
navigation: auto;
}
没了。链接从 page-a 跳到 page-b,浏览器自动做跨文档快照,然后 cross-fade 过渡。用户看到的不再是”撕掉重画”,而是流畅的淡入淡出。
为什么之前不行?浏览器切换文档是破坏性的,旧文档直接 unload,新文档从头创建 DOM。View Transitions 把这个过程拆成了两步:旧文档先快照,再 unload,新文档进来后再快照,然后浏览器自己在这两个快照之间做动画——你不需要写任何 JS 协调这些。
元素级过渡:让卡片自己”飞”过去
全页 cross-fade 其实挺无聊的。真正有意思的是给具体元素命名,让它跨页面变形——就像原生 App 里常见的那种”卡片展开成详情页”效果。
/* page-a.html:列表页 */
.product-card img {
view-transition-name: hero-image;
}
/* page-b.html:详情页 */
.detail-hero {
view-transition-name: hero-image;
}
两张页面里同一张图片有相同的 view-transition-name,浏览器就会自动追踪它的位置、大小、圆角变化,做一个 morph 动画——而不是两张独立的截图。苹果 WWDC 演示里用的就是这个效果,点击邮件列表里的邮件标题,标题自己”飞”进详情页的对应位置。
这个过程里,浏览器生成了一个临时的伪元素树:
::view-transition
└── ::view-transition-group(hero-image)
└── ::view-transition-image-pair(hero-image)
├── ::view-transition-old(hero-image) /* 旧页面截图 */
└── ::view-transition-new(hero-image) /* 新页面截图 */
你可以通过 ::view-transition-old/new() 自定义动画曲线、时长、缓动函数。默认是 ease 的 cross-fade,可以改成 slide、fade+scale 或者任何 @keyframes 定义的动画。
前进后退,动画不一样
如果你的网站有列表页和详情页,前进从列表进详情、后退从详情回列表,动画通常应该不同——前进滑入、后退滑出。这是 pageswap 和 pagereveal 两个事件要解决的问题。
// page-b.html(详情页)
window.addEventListener("pagereveal", async (e) => {
if (!e.viewTransition) return;
const from = new URL(navigation.activation.from.url);
const to = new URL(navigation.activation.entry.url);
// 从列表页进详情 → 前进动画
if (from.pathname.includes("/list")) {
e.viewTransition.types.add("slide-forward");
}
// 从详情退回列表 → 后退动画
else if (to.pathname.includes("/list")) {
e.viewTransition.types.add("slide-back");
}
});
然后在 CSS 里按类型分别写动画:
::view-transition-old(root).slide-forward {
animation: slide-out-left 0.3s ease-out both;
}
::view-transition-new(root).slide-forward {
animation: slide-in-right 0.3s ease-out both;
}
::view-transition-old(root).slide-back {
animation: slide-out-right 0.3s ease-out both;
}
::view-transition-new(root).slide-back {
animation: slide-in-left 0.3s ease-out both;
}
pageswap 在旧页面触发(你离开的时候),pagereveal 在新页面触发(你进来的时候)。两个事件里拿到的 e.viewTransition 是不同的对象,promise 行为也不同——pageswap 里的 .finished 在页面已经不可见时 resolve,pagereveal 里的 .ready 在快照拍完后 resolve。
四个坑,踩一个就白做了
1. 两页必须都 opt-in
旧页面写了 @view-transition 但新页面没写?动画不触发。设计系统里所有页面共享一个全局 CSS 文件可以缓解这个问题。
2. 同源限制
https://example.com 到 https://www.example.com(子域名不同)或者不同端口,动画不触发。这个限制是为了安全,防止跨域信息泄露。
3. 4 秒超时
Chrome 会在导航开始后 4 秒强制放弃动画,直接跳转。没有错误日志,只有一个 TimeoutError DOMException。可以监听 pagereveal 来捕获:
window.addEventListener("pagereveal", (e) => {
if (e.viewTransition) {
e.viewTransition.ready.catch((err) => {
if (err.name === "TimeoutError") {
console.log("导航超时,过渡动画被跳过");
}
});
}
});
4. BFCache 残留的 view-transition-name
从详情页点浏览器后退键,页面从 BFCache 恢复,之前在 pagereveal 里设置的 view-transition-name 还留在这个 DOM 节点上。再次前进时,如果同名元素在页面里有多处,动画直接跳过。所以在 .finished 之后一定要清理:
window.addEventListener("pagereveal", async (e) => {
if (!e.viewTransition) return;
const hero = document.querySelector(".detail-hero");
hero.style.viewTransitionName = "hero-image";
await e.viewTransition.ready;
hero.style.viewTransitionName = ""; // 快照拍完立即清掉
});
渐进增强:先求有,再求好
View Transitions 设计得最聪明的一点是:它天然是渐进增强。不支持的浏览器完全忽略 @view-transition,页面正常工作;支持的浏览器获得动画效果,不需要任何 JS 特性检测。
但主动做降级更好:
/* 尊重用户减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
@view-transition {
navigation: none;
}
}
/* 移动端小屏幕关掉复杂过渡 */
@media (max-width: 600px) {
@view-transition {
navigation: none;
}
}
落地路径
- 选一个访问频率最高的跳转路径(比如商品列表→详情),两个页面都加
@view-transition { navigation: auto; },验证基础 cross-fade 正常 - 给点击的元素加
view-transition-name,实现元素级 morph - 加
pageswap/pagereveal事件,按导航方向分类型写不同动画 - 清理
view-transition-name,支持 BFCache 场景 - 加上
prefers-reduced-motion媒体查询
整个过程不需要任何框架,不需要构建步骤,不需要 SPA 重构。现有的 MPA 网站,今天下午就能跑起来。
Safari 18.2 + Chrome 126 双支持的现在,这可能是过去五年里,浏览器免费送给前端工程师的最实用 UI 特性之一。
评论区
登录后可评论。