前端圈子里有一个需求被讨论了十几年:页面切换时能不能像原生App那样有个平滑的过渡动画?过去我们试过各种方案——Vue的transition组件、React的framer-motion、CSS动画配合路由钩子、甚至手写JavaScript去操作两个容器同时动画。这些方案都需要引入额外的依赖,而且在不同框架间迁移成本很高。
2024年,View Transitions API 正式在Chrome、Edge、Safari三大主流浏览器上获得了支持。它做的事情说起来很简单:在DOM状态变化前后分别截取一张快照,然后浏览器自动在这两张快照之间做平滑过渡。整个过程不需要你手动计算位置、不需要写复杂的动画关键帧,浏览器自己就能处理好。这篇文章就把这个API的核心用法、实际案例和踩过的坑完整梳理出来,让你看完就能落地。
一、一个极简入门:点击按钮让文字动起来
先别管什么页面切换,从最小的例子感受一下View Transitions的工作方式。假设页面上有一段文字,点击按钮后文字内容会变化,我们想让这个变化有个淡入淡出的过渡。
1.1 基本HTML结构
<div id="message">Hello World</div>
<button id="changeBtn">切换文字</button>
1.2 用startViewTransition包裹变化逻辑
document.getElementById('changeBtn').addEventListener('click', () => {
// 关键就在这里:把DOM更新逻辑放进回调函数里
document.startViewTransition(() => {
const msg = document.getElementById('message');
msg.textContent = msg.textContent === 'Hello World'
? '你好,世界'
: 'Hello World';
});
});
三行JavaScript,打开浏览器点击按钮,你会发现文字切换时自动带上了一个淡出再淡入的效果。浏览器在回调执行前截取了旧状态的快照,回调执行后截取新状态的快照,然后在两者之间执行默认的交叉淡入淡出过渡。
startViewTransition接受一个回调函数,这个回调里写的就是你原本要执行的DOM更新逻辑。浏览器会自动处理快照的捕获和动画的编排,你根本不需要关心动画的执行细节。
二、理解背后的两个伪元素树
View Transitions API在执行时会创建一个特殊的伪元素树,这个树位于页面顶层,覆盖在常规内容之上。它包含以下几层:
::view-transition
└─ ::view-transition-group(root)
├─ ::view-transition-image-pair(root)
│ ├─ ::view-transition-old(root) ← 旧状态快照
│ └─ ::view-transition-new(root) ← 新状态快照
└─ ...
过渡期间,浏览器把旧快照和新快照分别放进::view-transition-old和::view-transition-new这两个伪元素里,然后对它们应用默认的交叉淡入淡出动画。默认动画的持续时间大约250毫秒。
这个结构的重要性在于:你可以通过CSS覆盖这些伪元素的动画,实现完全自定义的过渡效果。比如下面这个例子,把默认的淡入淡出换成从右侧滑入:
::view-transition-old(root) {
animation: slide-out-left 0.3s ease-out forwards;
}
::view-transition-new(root) {
animation: slide-in-right 0.3s ease-out forwards;
}
@keyframes slide-out-left {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(-30px); opacity: 0; }
}
@keyframes slide-in-right {
from { transform: translateX(30px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
这里root代表整个页面的默认过渡组。后面会讲到如何给不同元素分配不同的过渡组,实现更精细的控制。
三、实战案例:产品列表到详情页的过渡
这是View Transitions真正能发挥的场景。假设有一个产品列表页,点击某个产品卡片后跳转到详情页,我们希望卡片上的缩略图能够“飞”到详情页的对应位置,形成一种连续性的视觉引导。
3.1 场景设定
列表页有一个产品卡片的缩略图,详情页在顶部也有同一张图。两张图的尺寸和位置不同,但它们的语义身份相同——都代表同一个产品的主图。View Transitions API通过view-transition-name这个CSS属性来匹配对应的元素。
3.2 在列表页给缩略图命名
<!-- 列表页 product-list.html -->
<div class="product-card">
<a href="product-detail.html?id=42" rel="external nofollow" >
<img src="product-42.jpg"
alt="产品42"
style="view-transition-name: product-image-42;">
<h3>产品名称42</h3>
</a>
</div>
3.3 在详情页给对应图片相同的命名
<!-- 详情页 product-detail.html -->
<div class="product-hero">
<img src="product-42.jpg"
alt="产品42"
style="view-transition-name: product-image-42;">
</div>
两个页面的图片都设置了相同的view-transition-name,浏览器就能识别它们是“同一个逻辑元素”,并在页面切换时自动为它们创建平滑的位置和大小过渡动画。
3.4 触发多页面视图过渡
在列表页点击链接跳转到详情页时,需要让浏览器知道这次导航需要执行视图过渡。对于同源页面跳转,在<head>中添加一个meta标签即可:
<meta name="view-transition" content="same-origin">
加上这个meta标签后,所有同源页面之间的导航都会自动启用视图过渡。浏览器在离开当前页面前截取快照,在新页面加载完成后截取新快照,然后执行过渡动画。整个过程完全不需要额外的JavaScript。
四、多页面导航的精细控制
same-origin的meta标签是一个全局开关,它让所有同源页面跳转都启用过渡。但有些情况下我们只想对特定的导航启用过渡,或者需要根据导航方向执行不同的动画。
4.1 用navigate事件替代meta标签
更灵活的方式是使用window.navigation API配合startViewTransition:
// 在列表页的脚本中
window.navigation.addEventListener('navigate', (event) => {
// 只对跳转到详情页的导航启用过渡
if (event.destination.url.includes('/product-detail')) {
event.transitionWhile(
(async () => {
// 这里可以加一些加载状态
await event.intercept({
handler: async () => {
await fetch(event.destination.url, {
// 实际项目中这里可能会做预加载等处理
});
}
});
})()
);
}
});
注意这个API涉及window.navigation,目前浏览器的支持还在推进中。对于大多数项目来说,先用meta标签全局开启已经足够,后续再逐步精细化。
4.2 过渡期间保持状态的技巧
多页面导航的一个常见挑战是:新页面加载需要时间,在这期间旧页面的快照还在屏幕上。如果新页面加载太慢,用户可能会看到旧快照停留很久然后突然切换。
解决方法是在新页面的<head>中尽可能早地设置view-transition-name相关的样式,确保浏览器在解析DOM时就能建立过渡元素的映射关系。另外,把关键图片的URL预加载或者使用<link rel="preload">也能显著缩短新页面渲染时间。
五、自定义过渡动画的完整案例
回到单页面场景。假设我们有一个图片画廊,点击缩略图后展开大图。缩略图和大图分别设置了相同的view-transition-name,点击时通过startViewTransition切换显示状态。
5.1 HTML结构
<div class="gallery">
<!-- 缩略图 -->
<div class="thumbnail">
<img id="thumb-img"
src="photo-thumb.jpg"
alt="风景照缩略图"
style="view-transition-name: gallery-photo;">
</div>
<!-- 大图(默认隐藏) -->
<div class="lightbox hidden" id="lightbox">
<img id="large-img"
src="photo-large.jpg"
alt="风景照"
style="view-transition-name: gallery-photo;">
<button id="closeBtn">关闭</button>
</div>
</div>
5.2 点击缩略图展开
document.getElementById('thumb-img').addEventListener('click', () => {
document.startViewTransition(() => {
const lightbox = document.getElementById('lightbox');
lightbox.classList.remove('hidden');
// 同时隐藏缩略图,让过渡显得更干净
document.getElementById('thumb-img').style.opacity = '0';
});
});
5.3 关闭大图
document.getElementById('closeBtn').addEventListener('click', () => {
document.startViewTransition(() => {
const lightbox = document.getElementById('lightbox');
lightbox.classList.add('hidden');
document.getElementById('thumb-img').style.opacity = '1';
});
});
运行这个例子,你会看到点击缩略图后,图片平滑地从缩略图位置过渡到大图位置;关闭时又从大图位置缩回到缩略图位置。这种效果以前需要用FLIP动画技术手动计算位置差,现在浏览器帮你做了所有计算。
六、多个元素同时过渡的编排
一个页面里可以有多个元素同时参与视图过渡,每个元素设置不同的view-transition-name即可。比如产品卡片除了主图还有标题和价格,点击后标题飞到头部的标题位置,价格飞到详情区域的价格位置。
<!-- 列表页的卡片摘要 -->
<article class="card">
<img src="prod.jpg" style="view-transition-name: prod-img-42;">
<h2 style="view-transition-name: prod-title-42;">产品名称</h2>
<span style="view-transition-name: prod-price-42;">¥299</span>
</article>
<!-- 详情页的对应区域 -->
<div class="detail-header">
<img src="prod.jpg" style="view-transition-name: prod-img-42;">
<h1 style="view-transition-name: prod-title-42;">产品名称</h1>
</div>
<div class="detail-sidebar">
<strong style="view-transition-name: prod-price-42;">¥299</strong>
</div>
浏览器会分别对prod-img-42、prod-title-42、prod-price-42这三个过渡组执行独立的过渡动画。它们的动画是并行进行的,时间线一致,不会出现某个元素先动、另一个后动的错位感。
一个重要的注意事项:同一个view-transition-name在页面中必须唯一。如果页面上有两个元素都命名为prod-img-42,浏览器会直接跳过这个过渡组,动画不会生效。在实际项目中,如果你用数据ID作为命名后缀(如上面的42),确保列表页和详情页用的ID一致即可。
七、过渡期间的交互处理
视图过渡持续的时间虽然短(通常200-500毫秒),但在这期间如果用户快速点击其他按钮,可能会产生冲突。浏览器对此有一些内置的保护机制。
7.1 过渡期间页面是冻结的
在startViewTransition的回调执行完成后,页面会立即渲染新状态。但过渡动画还在进行时,用户无法与新页面交互——点击、滚动等操作会被暂缓。这是为了保证旧快照和新快照的一致性。一旦动画完成,交互就会恢复正常。
7.2 用transition.finished等待完成
startViewTransition返回一个对象,包含finished和ready两个Promise:
const transition = document.startViewTransition(() => {
// DOM更新逻辑
});
// 过渡动画完全结束后执行
await transition.finished;
console.log('过渡动画已完成');
// 快照准备就绪、动画即将开始时执行
await transition.ready;
console.log('快照已捕获,动画开始');
transition.finished在需要串行执行多个过渡动画时非常有用。比如用户快速点击两次切换按钮,你可以先等第一次过渡完成再触发第二次,避免冲突。
八、浏览器兼容与渐进增强
View Transitions API的浏览器支持在2024年有了质的飞跃:
- Chrome 111+ 和 Edge 111+ 完整支持单页视图过渡(SPA)。
- Chrome 126+ 和 Edge 126+ 开始支持跨页面(MPA)的
same-origin过渡。 - Safari 18+ 在macOS和iOS上都已支持,这是关键的一步。
- Firefox目前仍在开发中,用户可以通过
layout.css.view-transitions.enabled标志开启。
不支持View Transitions的浏览器会直接忽略startViewTransition调用和相关CSS属性,DOM更新仍然正常执行,只是没有过渡动画。因此完全可以做渐进增强——先保证功能正常,再在支持的浏览器上添加动画:
if (document.startViewTransition) {
// 现代浏览器:使用过渡动画
document.startViewTransition(() => updateDOM());
} else {
// 旧浏览器:直接更新DOM
updateDOM();
}
这个兼容性检查很轻量,加在任何地方都不会有负担。
九、实际开发中踩过的几个坑
9.1 view-transition-name不要留空格
CSS自定义标识符不允许包含空格。如果你用了类似view-transition-name: product image 42这种写法,过渡不会生效。建议用连字符或下划线连接,或者直接拼接成驼峰式。
9.2 过渡组名称过多会影响性能
浏览器需要为每个view-transition-name创建一个独立的快照。如果页面上有上百个元素都设置了过渡名称,内存占用和快照捕获时间都会显著增加。实际项目中建议只给真正需要动画的元素(通常是关键视觉节点)设置过渡名称,不要全页面铺开。
9.3 背景色突变的问题
新页面和旧页面的背景色如果差异很大,过渡时可能会出现不自然的颜色跳变。这时候可以给::view-transition-group(root)设置一个较短的持续时间,或者让背景色也参与过渡。
十、总结
View Transitions API最让我兴奋的地方不是它能做多炫的动画,而是它把动画逻辑从应用层提升到了浏览器层。以前前端框架的过渡方案需要记住两套状态、手动计算位置差、处理动画结束的回调,现在只需要把变化前后的DOM交给浏览器,剩下的自动完成。
适合用这个API的场景很明确:页面切换、列表到详情的过渡、弹窗展开和收起、标签页切换等任何涉及DOM结构变化的视觉状态转换。对于静态内容的页面(比如纯展示型官网),多页面模式下的same-origin过渡可能是最简单的落地方式;对于交互复杂的后台系统,单页面内的startViewTransition配合自定义CSS动画则更加灵活。
现在主流浏览器的支持已经覆盖了绝大多数用户,正是把这个API用到实际项目里的好时机。从最小的一个按钮开始试试看,你会发现写页面切换动画这件事从来没有这么简单过。

