一、页面过渡曾经有多麻烦
你一定见过这样的场景:从一个列表页点击进入详情,屏幕瞬间闪白,新内容突兀地出现。为了让这个过程更自然,开发者们绞尽脑汁——要么用JavaScript拦截点击,手动做DOM的淡入淡出,还得处理浏览器的历史记录;要么在单页应用里用路由守卫配合复杂的动画库。这些方案不仅代码量不小,而且稍有不慎就会出现闪烁或卡顿。
但现在情况不一样了。浏览器原生的 View Transitions API 来了。它允许我们只用几行CSS就告诉浏览器:“我准备切换视图了,请帮我在这两帧之间做一个平滑的动画。” 浏览器会自动截取当前页面状态的快照,再截取新页面的快照,然后在这两张静态图之间做交叉淡入淡出——整个过程发生在合成器线程上,性能极佳,而且不用写一行JavaScript。
这篇文章会带着你从最基本的页面切换开始,到最后实现一个自定义动画的图片画廊,把 View Transitions API 吃透。
二、最简示例:让页面切换“动”起来
View Transitions API 的使用门槛低得令人惊讶。假设你有一个多页面应用(每个页面都是独立的HTML文件),你想让用户在页面之间导航时有一个平滑的淡入淡出效果。你只需要做一件事——在全局样式表里加上这么一段:
@view-transition {
navigation: auto;
}
放进去之后,当你点击链接切换到同源的其他页面时,浏览器就会自动处理接下来的事情:它会在离开旧页面时拍一张快照,进入新页面后再拍一张,然后用默认的“交叉淡入淡出”动画把两张快照衔接起来。旧页面逐渐消失,新页面逐渐出现,整个过程大约持续200毫秒,非常自然。
这里要注意一个细节:navigation: auto 告诉浏览器,只要导航发生在同源文档之间,就激活视图过渡。如果不想全局启用,也可以单独对某个链接或按钮触发,但那就需要一点点JavaScript:
function navigateTo(url) {
document.startViewTransition(() => {
window.location.href = url;
});
}
不过,这篇文章想展示的是“零脚本”方案,所以我们就用 navigation: auto 好了。
三、理解幕后:快照、伪元素与动画组
启用了视图过渡后,浏览器在后台悄悄做了几件事。它创建了一个特殊的伪元素树,里面包含两个关键角色:
- ::view-transition-old(root) —— 代表过渡开始时的页面快照。
- ::view-transition-new(root) —— 代表过渡结束时的页面快照。
默认的动画就是让 old 从 opacity: 1 变到 0,同时让 new 从 opacity: 0 变到 1。你可以用开发者工具的元素面板看到这些伪元素,但它们是浏览器内部生成的,不能直接在常规的CSS选择器之外操作。
有意思的是,我们可以利用这些伪元素来自定义动画。比如,你想让旧页面往左滑出,新页面从右边滑入,可以这样写:
::view-transition-old(root) {
animation: slide-out 0.4s ease-in both;
}
::view-transition-new(root) {
animation: slide-in 0.4s ease-out both;
}
@keyframes slide-out {
to { transform: translateX(-30px); opacity: 0; }
}
@keyframes slide-in {
from { transform: translateX(30px); opacity: 0; }
}
这样,页面导航就不再是生硬的切换,而像是翻了一页书。
四、实战:图片画廊的平滑展开
光有页面间的过渡还不够,真正的交互场景往往是同一个页面内的视图变化。比如一个图片画廊:点一下缩略图,图片放大展示。在过去,我们需要写一堆JavaScript来处理状态的改变和动画;现在,只需要把视图过渡和一点儿DOM更新结合起来就行了。
我们先构建一个简单的画廊结构。注意,我们不会用路由或多页,而是在同一个页面中用CSS控制显示/隐藏(或者用少量JS切换类名)。即便如此,视图过渡仍能让我们在两个状态之间获得丝滑动画。
HTML 大致如下:
<div class="gallery">
<div class="thumb">
<img src="cat.jpg" alt="一只猫" data-full="cat-full.jpg">
</div>
<div class="thumb">
<img src="dog.jpg" alt="一只狗" data-full="dog-full.jpg">
</div>
<!-- 更多缩略图 -->
</div>
<div class="lightbox hidden" id="lightbox">
<img id="lightbox-img" src="" alt="">
<button class="close">关闭</button>
</div>
我们还需要一点点 JavaScript 来处理状态切换,因为纯CSS目前还无法很好地处理点击展开这种交互。但这个脚本只是切换 class,视图过渡的工作仍然是 CSS 主导。
const lightbox = document.getElementById('lightbox');
const lightboxImg = document.getElementById('lightbox-img');
const thumbs = document.querySelectorAll('.thumb img');
thumbs.forEach(thumb => {
thumb.addEventListener('click', () => {
const fullSrc = thumb.dataset.full;
if (document.startViewTransition) {
document.startViewTransition(() => {
lightboxImg.src = fullSrc;
lightbox.classList.remove('hidden');
});
} else {
// 降级处理
lightboxImg.src = fullSrc;
lightbox.classList.remove('hidden');
}
});
});
document.querySelector('.close').addEventListener('click', () => {
lightbox.classList.add('hidden');
});
代码里用到了 document.startViewTransition,它接受一个回调,在回调里我们更新DOM。浏览器会自动处理快照和动画。现在画廊的展开动作瞬间就变得优雅了,轻量箱(lightbox)会从缩略图的位置“放大”出来。
五、自定义过渡:让元素“飞”到该去的地方
默认的交叉淡入淡出虽然不错,但还缺少那种“元素连续变换”的感觉。比如我们想实现一个效果:点击缩略图时,小图放大并移动到灯箱中央。这就得给参与过渡的元素加一个 view-transition-name。
我们给每个缩略图对应的图片赋予相同的过渡名称,这样浏览器就会把它们视为“同一个元素”在视图切换前后的两种状态,从而自动生成连续的变形动画。
.thumb img {
view-transition-name: gallery-image;
}
但这会导致一个问题:页面上有好几张缩略图,如果它们拥有相同的 view-transition-name,浏览器就不知道哪个对应哪个。所以我们需要动态地、唯一地分配名称。这可以通过在 JavaScript 中点击时临时设置来解决:
thumb.addEventListener('click', () => {
// 为当前缩略图设置过渡名称
thumb.style.viewTransitionName = 'active-image';
const fullSrc = thumb.dataset.full;
if (document.startViewTransition) {
document.startViewTransition(() => {
lightboxImg.src = fullSrc;
lightboxImg.style.viewTransitionName = 'active-image';
lightbox.classList.remove('hidden');
});
}
// 过渡结束后清除名称,避免干扰下次
lightbox.addEventListener('transitionend', () => {
thumb.style.viewTransitionName = '';
lightboxImg.style.viewTransitionName = '';
}, { once: true });
});
这样一来,当你点一只猫的缩略图时,浏览器会捕捉到那个小图的位置和尺寸,然后在新状态里找到带有同样名称的大图,自动计算出位移、缩放和透明度变化,并生成一个流畅的动画。整个过程就像原生APP里的共享元素过渡。
六、细化动画节奏,让它更有质感
有了基本的共享元素过渡,我们还可以通过CSS进一步调整动画的缓动函数和持续时间,让效果更贴合设计意图。针对 ::view-transition-group(active-image) 这个伪元素,我们可以覆盖默认的动画曲线。
::view-transition-group(active-image) {
animation-timing-function: cubic-bezier(0.2, 0.9, 0.3, 1.2);
animation-duration: 0.5s;
}
如果你想玩得更细腻,还可以分别控制旧视图和新视图的动画。比如让旧缩略图在放大的同时稍微变暗,新大图从缩略图的位置“长”出来。
::view-transition-old(active-image) {
animation: scale-up-and-fade 0.4s ease-in both;
}
::view-transition-new(active-image) {
animation: scale-up 0.4s ease-out both;
}
@keyframes scale-up-and-fade {
from { transform: scale(1); opacity: 1; }
to { transform: scale(1.5); opacity: 0; }
}
@keyframes scale-up {
from { transform: scale(0.1); }
to { transform: scale(1); }
}
这些调整都不需要触动HTML结构,完全交由样式控制,维护起来很轻松。
七、回归多页应用:把画廊扩成真正的页面过渡
如果你做的不是单页内的灯箱,而是从列表页跳转到详情页(两个独立的HTML文档),那么“共享元素过渡”也是可行的。原理一样:在列表页的每个缩略图上设置唯一的 view-transition-name,在详情页对应的大图上设置相同的名称。再配合全局的 @view-transition { navigation: auto },浏览器就会在页面导航时自动处理过渡。唯一需要注意的是,跨文档的共享元素过渡要求名称在页面间保持一致,这通常需要一点模板变量在后端渲染时注入,或者用URL参数来传递。
不过,今天我们的重点还是在单页内实现无路由的丝滑体验,毕竟对于很多轻量级项目来说,没有路由库反而更简单。
八、兼容性提醒与降级策略
View Transitions API 在 Chromium 111 及以上版本已经得到支持(包括Edge、Opera等),Safari 从 18.0 开始也加入了支持,Firefox 目前还在实验性阶段。所以对于生产环境,我们需要一个简单的降级方案。前面代码里也提到了,用 if (document.startViewTransition) 检测特性,如果浏览器不支持,就直接更新DOM,没有任何动画——虽然逊色一点,但功能完全不受影响。
九、写在最后
视图过渡API给我的感觉是:它把前端动画领回了正轨。过去我们为了一个缩放效果可能要引入整个GSAP动画库,现在用寥寥几行CSS就能做到。更妙的是,这一切都是由浏览器优化过的,哪怕是在低性能设备上也能跑得很稳定。
今天我们一起走过了从全局页面过渡到单页画廊的完整路径,相信你已经可以动手在自己项目里尝试了。当你看到那一瞬间小图变大图、列表平滑翻页的时候,大概就会和我一样觉得——这才叫现代化Web体验。

