页面一跳转就要等浏览器撕掉重画?这件事今天被两个 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 定义的动画。

前进后退,动画不一样

如果你的网站有列表页和详情页,前进从列表进详情、后退从详情回列表,动画通常应该不同——前进滑入、后退滑出。这是 pageswappagereveal 两个事件要解决的问题。

// 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.comhttps://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;
  }
}

落地路径

  1. 选一个访问频率最高的跳转路径(比如商品列表→详情),两个页面都加 @view-transition { navigation: auto; },验证基础 cross-fade 正常
  2. 给点击的元素加 view-transition-name,实现元素级 morph
  3. pageswap/pagereveal 事件,按导航方向分类型写不同动画
  4. 清理 view-transition-name,支持 BFCache 场景
  5. 加上 prefers-reduced-motion 媒体查询

整个过程不需要任何框架,不需要构建步骤,不需要 SPA 重构。现有的 MPA 网站,今天下午就能跑起来。

Safari 18.2 + Chrome 126 双支持的现在,这可能是过去五年里,浏览器免费送给前端工程师的最实用 UI 特性之一。

评论区

0 条评论

登录后可评论。