View Transitions API 是浏览器原生的页面过渡方案,让多页应用(MPA)和单页应用(SPA)都能在不引入动画库的情况下实现丝滑的视图切换。过去页面跳转总是“硬切”——旧内容瞬间消失、新内容瞬间出现,体验割裂;而 View Transitions API 通过对新旧两帧截图做交叉淡入淡出,把这种割裂变成流畅的过渡动画。更妙的是它原生支持“共享元素”:列表里的小图能在路由切换后平滑“飞”成大图,全程无需计算坐标。本文从原理到代码,带你实战用原生 API 给网站加上过渡效果。
一、它解决了什么痛点
传统页面跳转靠浏览器默认的“白屏替换”:MPA 每次导航都整页刷新,SPA 则由框架手动替换 DOM。SPA 能用 Vue 的 <Transition> 或 Framer Motion 做组件级动画,但多页应用几乎无能为力——除非引入沉重的客户端路由。View Transitions API 把“截图—过渡—替换”的能力下沉到浏览器,MPA 和 SPA 第一次有了统一的、零依赖的过渡方案。
它最大的价值是“共享元素动画”:列表页的小图,点击后在详情页“飞”成大图,位置与尺寸由浏览器自动补间,无需手写任何坐标计算。在注重体验的技术站里,这种细节正是留住读者的关键,也是过去只能用商业动效库才能勉强实现的效果。
二、核心机制:截图 + 交叉过渡
2.1 document.startViewTransition()
SPA 场景下,把“会改 DOM 的更新函数”包进 startViewTransition,浏览器先对当前页面截图(old 快照),执行回调更新 DOM 后再对新页面截图(new 快照),最后在两者间播放过渡:
if (document.startViewTransition) {
document.startViewTransition(() => updateDOM());
} else {
updateDOM(); // 不支持则直接更新,无动画
}
理解“快照模型”很关键:两次截图都是位图。这意味着过渡期间,正在播放的视频、CSS 动画、Canvas 都会被“冻结”成静态帧——它们只在过渡结束后才恢复活动。如果需要对某块内容保持动态,应给它单独命名并排除在根过渡之外,而不是指望它在过渡中继续动。
2.2 伪元素树与 view-transition-name
过渡运行时,浏览器会在文档顶部插入一棵 ::view-transition 伪元素树:::view-transition-group(name) 负责定位与尺寸补间,::view-transition-image-pair(name) 包裹新旧两帧,::view-transition-old(name) / ::view-transition-new(name) 分别是旧、新截图。默认整页作为 root 一起过渡;给具体元素分配全局唯一的 view-transition-name,浏览器会把它单独抽出来做补间,从而与根过渡解耦。
三、多页应用实战:给原生站点加过渡
MPA 无需手写 JS——浏览器在同源导航前后自动截图。你只需声明启用,并为 root 指定动画。结合 原生 CSS 嵌套 等新特性,现代 CSS 已能独立完成整套过渡。
@view-transition {
navigation: auto;
}
::view-transition-old(root) {
animation: 300ms ease-out both fade-out;
}
::view-transition-new(root) {
animation: 300ms ease-in both fade-in;
}
@keyframes fade-out { to { opacity: 0; } }
@keyframes fade-in { from { opacity: 0; } }
3.1 浏览器支持与渐进增强
目前 Chromium 系已稳定支持,Firefox/Safari 在跟进。用特性检测做渐进增强,不支持的浏览器自动降级为“无动画直跳”,不影响功能:
const supportsVT = "startViewTransition" in document;
console.log("View Transitions 支持:", supportsVT);
3.2 跨文档过渡的“双向声明”
MPA 的跨文档过渡有个容易被忽略的点:只有当前页和新页面都声明了 @view-transition { navigation: auto },浏览器才会动画化这次导航;任一方缺省都会退化为普通跳转。此外跨文档过渡要求同源(same-origin),跨域跳转无法享受该能力,因此站内的文章详情、标签页切换最适合首批接入。
四、单页应用实战:拦截路由更新
SPA 没有真实导航,必须手动包裹 DOM 更新。下面把路由回调包进一个 navigate helper,并等待过渡结束再继续:
function navigate(updateDom) {
if (!document.startViewTransition) return updateDom();
return document.startViewTransition(updateDom).finished;
}
// 在路由回调中调用
router.on("/about", () => navigate(() => renderAbout()));
4.1 自定义动画时长与缓动
你可以用 ::view-transition-group(root) 统一控制位移/缩放的时长与缓动;用 ::view-transition-old/new(root) 控制淡入淡出。把根过渡和共享元素过渡分开调,能做出“背景快切、重点元素慢飞”的层次感:
::view-transition-group(root) {
animation-duration: 400ms;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
五、共享元素动画:让图片“飞”过去
给前后两页同一元素分配相同的 view-transition-name,浏览器就自动在位置与尺寸间补间。典型场景:列表缩略图 → 详情大图。
<!-- 列表页 -->
<img class="thumb" style="view-transition-name: hero-1" src="cover.jpg">
<!-- 详情页 -->
<img class="hero" style="view-transition-name: hero-1" src="cover.jpg">
当列表项很多时,不能写死 name,要在交互瞬间动态分配,并在过渡结束后清理,避免污染后续切换:
listItems.forEach((el, i) => {
el.style.viewTransitionName = `item-${i}`;
});
// 过渡结束清掉,确保下次快照唯一
document.startViewTransition(update).finished.finally(() => {
listItems.forEach(el => (el.style.viewTransitionName = ""));
});
⚠️ 铁律:同一快照状态下,name 必须全局唯一。若两个元素在捕获瞬间共享了同一个 view-transition-name,浏览器会直接跳过该过渡并在控制台报错。所以动态命名后务必在 .finished 时把 name 清空。
六、性能与降级
过渡本质是“整页截图 + 合成动画”,超长页面或含大量视频/Canvas 时开销明显。务必做能力检测,不支持时直接更新:
if (!document.startViewTransition) {
updateDOM(); // 静默降级,无动画
}
同时尊重无障碍偏好——对 prefers-reduced-motion 用户直接关闭动画,而不是只调短时长:
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
- 动画时长控制在 200–400ms,过久反而拖沓
- 只对关键视图命名,避免全页大图参与过渡
- 谨慎对待含 iframe、大型 Canvas 的根过渡,缩小截图范围
七、与框架 / 构建链集成
在 Vue 3 项目里,可在路由守卫返回 startViewTransition 的 Promise;React 可用一个 hook 包裹 setState;Next.js、Astro 等现代框架已内置对 View Transitions 的一等支持。开发体验则依赖 Vite/Rspack 这类现代构建工具的热更新,想提效也可参考 VS Code 插件 清单优化编辑器。
View Transitions API 把“丝滑过渡”从框架特权变成了浏览器标配。无论你维护的是原生多页站点,还是在 SPA 里追求更顺滑的路由体验,它都值得立刻用起来——零依赖、可降级、几行 CSS 就能见效,是 2026 年最值得上车的原生前端能力之一。
八、调试与常见报错
- DevTools 的 Animations 面板可录制并回放 view transition,方便微调时长与缓动。
- 控制台报 name 重复或 “At least two different captured states”:检查
view-transition-name是否在同屏唯一。 - 过渡完全不触发:确认是否同源、两个页面是否都声明了
@view-transition、以及是否真的发生了 DOM/导航变更。 - 性能异常:避免对含 iframe、大型 Canvas 的根做长动画;必要时只给关键卡片命名,缩小截图范围。




