单页应用的页面切换动画一直是前端体验里很重要的一环。过去我们要么用 CSS Transition 配合路由库的生命周期自己写,要么引入像 Animate.css 或 Motion One 这样的库。但无论哪种方案,都得手动管理两个页面(或组件)的进入和离开状态,代码一多就容易出问题。Chrome 111 在 2023 年 3 月正式支持了 View Transitions API,到 2025 年,这个特性已经非常稳定,Safari 和 Firefox 也在跟进。它让你用几行 JavaScript 就能实现类似原生应用的页面过渡效果,连截图和动画都由浏览器自己搞定。这篇文章带你把它的基本用法摸透,然后写一个完整的多页面切换示例。
一、我们先看一下旧方案有多累
假设你有一个简单的 SPA,通过点击导航链接切换页面。传统的实现思路是这样的:点击链接时,先给当前页面添加一个退出动画,动画结束后销毁当前页面,同时创建新页面并设置一个进入动画。这背后需要你维护动画状态、计算时间、处理中断。如果你用的不是 Vue 的 <Transition> 或 React 的 framer-motion,纯手写的话,很容易出现动画不同步、闪烁,或者用户在动画期间点击其他链接导致状态错乱。
View Transitions API 换了一种角度。你只需要告诉浏览器”我要更新 DOM 了”,然后浏览器会自动完成以下步骤:
- 给当前页面拍一张快照。
- 执行你的 DOM 更新逻辑。
- 给更新后的页面再拍一张快照。
- 用旧快照和新快照自动生成平滑的过渡动画。
你不需要手动操作任何动画属性。那个感觉就像在原生应用里 push 了一个新页面——默认会有一个淡入淡出或滑动的效果。
二、最简单的例子:点击按钮,文本切换带过渡
我们从一个极简的 demo 开始,感受一下这个 API 的魔力。下面的 HTML 里只有一个标题和一个按钮,点击按钮后标题文字会变,但变化过程是平滑的交叉淡入淡出。
<h1 id="title">Hello World</h1>
<button id="changeBtn">更换文字</button>
<script>
const titleEl = document.getElementById('title');
const btn = document.getElementById('changeBtn');
btn.addEventListener('click', () => {
// 判断浏览器是否支持
if (!document.startViewTransition) {
titleEl.textContent = '你好,世界';
return;
}
// 核心:把 DOM 更新放进回调里
document.startViewTransition(() => {
titleEl.textContent = '你好,世界';
});
});
</script>
在支持 View Transitions API 的浏览器里点击按钮,你会看到旧文字淡出,新文字淡入。就这么简单。如果浏览器不支持,document.startViewTransition 是 undefined,代码会直接更新 DOM,不会报错。所以它天然就是渐进增强的。
这个默认的过渡效果是全局的:浏览器会截取整个视口,然后做交叉淡入淡出。对于文字这种小范围变化,整个页面淡入淡出有点太重了。好在 API 允许我们限定过渡的范围,还要自定义动画。
三、限定过渡范围和自定义动画
浏览器在执行视图过渡时,会自动为快照中的每个元素(如果有变化)生成一组伪元素:
::view-transition-old(root)—— 旧状态的根快照::view-transition-new(root)—— 新状态的根快照
你可以通过给想要参与过渡的元素设置 view-transition-name 属性,来让浏览器只为这个特定元素创建过渡快照,而不是整页。然后就可以在 CSS 里用这些伪元素来定制动画。
3.1 给元素命名
<h1 id="title" style="view-transition-name: my-title;">Hello World</h1>
注意:view-transition-name 的值在整个页面里必须是唯一的。如果有两个元素同时用了同一个名字,浏览器会抛异常。
3.2 在 CSS 里自定义动画
::view-transition-old(my-title) {
animation: fade-out 0.3s ease-out forwards;
}
::view-transition-new(my-title) {
animation: fade-in 0.3s ease-out forwards;
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
这样一来,只有标题元素会参与过渡,背景和其他内容保持不变。动画的时机和形式完全由你控制。
四、实战:多页面切换的 SPA 过渡
上面的例子只是换了一行字。真实场景里,页面切换往往伴随着多个元素的变化:标题变了、内容区整个都不同了、导航高亮也变了。我们把 View Transitions API 用在一个简单的多页面路由上。
4.1 项目结构
我们要做一个包含首页、列表页和详情页的单页应用。点击顶部导航切换页面,切换时旧页面内容区淡出并向左平移,新页面内容区从右侧滑入并淡入。标题和底部栏保持不变。
4.2 HTML 骨架
<nav>
<button data-route="home" class="nav-btn active">首页</button>
<button data-route="list" class="nav-btn">列表</button>
<button data-route="detail" class="nav-btn">详情</button>
</nav>
<main id="app" class="view-root">
<div class="page" id="home">
<h2>欢迎来到首页</h2>
<p>这里是最新的内容推荐。</p>
</div>
<div class="page" id="list" style="display:none;">
<h2>列表页</h2>
<ul>
<li>文章 A</li>
<li>文章 B</li>
<li>文章 C</li>
</ul>
</div>
<div class="page" id="detail" style="display:none;">
<h2>详情页</h2>
<p>这里是文章的详细内容。</p>
</div>
</main>
4.3 核心 JavaScript 路由器
const navButtons = document.querySelectorAll('.nav-btn');
const pages = {
home: document.getElementById('home'),
list: document.getElementById('list'),
detail: document.getElementById('detail'),
};
let currentRoute = 'home';
// 切换路由的函数
function navigateTo(route) {
if (route === currentRoute) return;
// 更新按钮高亮
navButtons.forEach(btn => {
btn.classList.toggle('active', btn.dataset.route === route);
});
// 如果浏览器不支持 View Transitions,直接切换
if (!document.startViewTransition) {
switchPage(route);
return;
}
// 启动视图过渡
const transition = document.startViewTransition(() => {
switchPage(route);
});
// 可以接收 transition.finished 来做过渡完成后的处理(这里暂时不用)
}
function switchPage(route) {
// 隐藏所有页面
Object.values(pages).forEach(p => p.style.display = 'none');
// 显示目标页面
pages[route].style.display = 'block';
currentRoute = route;
}
// 绑定导航事件
navButtons.forEach(btn => {
btn.addEventListener('click', () => {
navigateTo(btn.dataset.route);
});
});
4.4 配合 CSS 定义页面过渡动画
为了让页面内容区有滑动效果,我们需要给容器(main)一个 view-transition-name,并定义对应的旧/新快照动画。特别要注意,每次页面切换时,因为容器内容变了,浏览器会为这个容器创建过渡快照。我们在 CSS 里这样写:
/* 给内容区一个过渡名称 */
.view-root {
view-transition-name: main-content;
}
/* 旧内容:向左滑出并淡出 */
::view-transition-old(main-content) {
animation: slide-out 0.3s ease-in forwards;
}
/* 新内容:从右侧滑入并淡入 */
::view-transition-new(main-content) {
animation: slide-in 0.3s ease-out forwards;
}
@keyframes slide-out {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(-30px);
opacity: 0;
}
}
@keyframes slide-in {
from {
transform: translateX(30px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
现在点击顶部导航按钮,你会看到内容区像原生应用一样滑动切换。标题和底部栏因为没有设置 view-transition-name,所以不受影响,保持不动。整个过程中我们没有手动写任何动画启动或关闭的逻辑——切换页面就一行 document.startViewTransition。
五、踩坑和需要注意的细节
当我自己第一次在生产项目里用这个 API 时,遇到了几个容易忽略的点:
1. 同一名称必须全局唯一。如果页面中有两个元素同时使用了相同的 view-transition-name,调用 startViewTransition 会立即抛出 DOMException。所以当你用组件化开发时,要保证动态渲染的元素不会命名冲突。一个常用技巧是:只有在切换动画期间才给元素添加 view-transition-name,或者使用路由相关的 key 来动态生成名称。
2. 页面快照是静态位图。::view-transition-old 实际上是一张静态截图,过渡期间它不会再响应 DOM 变化。如果你的旧页面里有 gif 动画或视频,过渡时会冻结在旧状态。这是特意设计的,目的是保证过渡的平滑和性能。如果你不希望冻结,就得考虑其他方式。
3. 过渡期间可以继续交互吗?默认情况下,视图过渡进行时页面仍可交互,但某些操作(比如再次启动一个新的视图过渡)会被跳过,直到当前过渡完成。浏览器提供了一个 skipTransition() 方法可以立即结束过渡,但最好等待自然完成。
4. 跨页面过渡(MPA)也支持。Chrome 126+ 开始支持跨文档视图过渡,即从页面 A 导航到页面 B(非 SPA)也能有平滑动画。需要在两端的页面头部添加 <meta name="view-transition" content="same-origin">。这个特性目前还比较新,Safari 和 Firefox 还未实现,但方向已经明确。
六、和框架集成的一点点思路
在 Vue 或 React 中,你可以把视图过渡的启动位置放在路由守卫或者状态更新的回调里。比如 Vue Router 的 beforeEach 里不适合直接调用 startViewTransition,因为这时 DOM 还没更新。正确的做法是在组件内部,当你改变响应式数据来展示新页面内容时,用 nextTick 或者 flushSync(React)确保 DOM 更新发生在视图过渡的回调里。
一个通用的封装思路是:创建一个自定义 hook 或组合式函数,把 startViewTransition 包裹起来,并处理命名冲突和动画定义。这能让团队在不同页面间复用一致的过渡效果。
七、总结
View Transitions API 把页面切换动画从”手写状态机”降维到了”声明意图”。你不再需要关心旧页面什么时候销毁、新页面什么时候挂载,只需要在 DOM 更新之前告诉浏览器一声,剩下的截图、混合、动画执行都由浏览器底层处理。默认效果已经足够好,自定义的灵活度也完全不输第三方库。
对于还在用各种动画库处理路由过渡的项目,可以逐步尝试用这个原生 API 替代。它体积为零,性能更好,而且随着浏览器支持面的扩大,将来会成为 SPA 转场的标配手段。文中的代码在 Chrome 126+ 上都是直接可运行的,不妨复制一份到你的本地项目里试试——那种旧页面滑出、新页面滑入的丝滑感,会让你瞬间忘记以前为了加个动画写了多少行代码。

