View Transitions:让同一个元素在两个状态间「长」过去
从一个缩略图点进详情页,如果界面硬切,用户得在新画面里重新找回「我刚点的是哪个」。共享元素转场 (Shared Element Transition) 的思路:告诉浏览器前后两个状态里哪个元素是同一个,剩下的位移 +
缩放中间帧由它自动补出——缩略图会平滑地长成大图,视线被这条连续的运动一路带过去。原生 document.startViewTransition() 让这件事不再需要手写动画。下面对比开 / 关转场。
机制只有两步:给缩略图和详情大图的同一元素标上相同的 view-transition-name;把切换 DOM 的操作包进 startViewTransition(callback)。浏览器会在回调前后各截一张图,对同名元素自动生成 transform +
clip 的补间,其余内容默认做交叉淡入淡出。整个过程不写一行 keyframes,且可被 ::view-transition-group() 进一步定制时长与缓动。
两步:命名 + 包住 DOM 更新——浏览器补出中间帧:
/* 1. 给前后两个状态的"同一元素"标同名 */
.thumb, .hero { view-transition-name: active-media; }
// 2. 把切换 DOM 的操作包进 startViewTransition
function openDetail(item) {
if (!document.startViewTransition) return render(item); // 降级
document.startViewTransition(() => render(item)); // 自动补间
}
1 · 跨文档转场:连真正的页面跳转也能补间
上面是同文档转场——DOM 没换、URL 没变,只是 JS 改了一段节点。但 View Transitions 真正打开局面的是跨文档(MPA):从一个 HTML 页面真的导航到另一个,中间也能补出连续动画。而且它一行 JS 都不用——只要导航的前后两个文档同源、且各自都写上这条 at-rule:
/* page-a.html 和 page-b.html 都写这一行 —— 同源即可 */
@view-transition { navigation: auto; }
/* 想让某个元素跨页"长"过去,就在两页给它同一个名字 */
.thumb { view-transition-name: vt-hero; } /* Page A 的小图 */
.hero { view-transition-name: vt-hero; } /* Page B 的大图 */
下面这个内嵌站点有两个真实页面(相册 ↔ 详情),都开了 @view-transition。点「查看详情 / 返回」是 iframe 内一次真正的跨文档导航(URL 真的变了),那块封面会从小图位置长成大图——靠的是两页同名的
view-transition-name: vt-hero。右上角切换「默认 crossfade」与「圆形 clip-path 擦除」两种整页效果:
转场期间浏览器把页面快照拆成一棵伪元素树:每个 view-transition-name 各占一个 group,旧、新两帧叠在 image-pair 里交叉淡入淡出。想定制哪段动画(时长、缓动、上面的 clip-path 擦除),就用 @keyframes 命中对应伪元素:
用 @keyframes 把整页 crossfade 换成圆形擦除:
::view-transition-old(root) { animation: none; } /* 旧页定住当背景 */
::view-transition-new(root) { /* 新页圆形扩开盖上来 */
animation: 600ms vt-reveal cubic-bezier(.2, .6, .2, 1);
}
@keyframes vt-reveal {
from { clip-path: circle(0% at 50% 80%); }
to { clip-path: circle(150% at 50% 80%); }
}
三个约束: 其一,同一时刻一个 view-transition-name 只能属于一个可见元素(否则报错,本页用「隐藏另一侧」规避);其二,startViewTransition 截的是整页快照,转场期间页面短暂不可交互;其三,它对应本系列「用运动替代跳变」的主题,但动效要克制——并尊重 prefers-reduced-motion(浏览器会自动跳过)。