View Transitions:让同一个元素在两个状态间「长」过去
从一个缩略图点进详情页,如果界面硬切,用户得在新画面里重新找回「我刚点的是哪个」。共享元素转场 (Shared Element Transition) 的思路是告诉浏览器前后两个状态里哪个元素是同一个,剩下的位移与缩放中间帧由它自动补出——缩略图会平滑地长成大图,视线被这条连续的运动一路带过去。原生
document.startViewTransition() 让这件事不再需要手写动画。
机制只有两步:给缩略图和详情大图标上相同的 view-transition-name;把切换 DOM 的操作包进 startViewTransition(callback)。浏览器在回调前捕获旧状态、回调后捕获新状态,对同名元素自动生成 transform 与 clip 的补间,其余内容默认做交叉淡入淡出。整个过程不写一行
keyframes,且可被 ::view-transition-group() 进一步定制时长与缓动。
第一步是命名,写在 CSS 里:
/* 给前后两个状态的「同一元素」标同名 */
.thumb,
.hero { view-transition-name: active-media; }
第二步是把 DOM 更新包起来,浏览器负责补出中间帧:
function openDetail(item) {
if (!document.startViewTransition) return render(item); // 降级: 直接切
document.startViewTransition(() => render(item)); // 自动补间
}
注 · 前后两帧的性质并不对称。按 MDN 的说法,旧状态被捕获成一张静态图片,用户在它淡出期间无法与之交互;新状态则是一块实时的、可交互的 DOM 区域,动画还在跑时就已经能点。所以转场期间「整页冻住」的说法并不准确——真正盖在上面的是 ::view-transition 这层浮层,UA 样式给它
position: fixed; inset: 0,铺满视口。
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 的大图 */
图 1-1 的内嵌站点有两个真实页面(相册与详情),都开了 @view-transition。点「查看详情 / 返回」是 iframe 内一次真正的跨文档导航,URL 真的变了,那块封面会从小图位置长成大图——靠的是两页同名的 view-transition-name: vt-hero。
转场期间浏览器把捕获结果拆成一棵伪元素树:每个 view-transition-name 各占一个 group,旧、新两帧叠在 image-pair 里交叉淡入淡出。想定制哪段动画,就用 @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%); }
}
警示 · prefers-reduced-motion 需要自己处理,浏览器不会代劳。MDN 的 View Transition API 文档里,唯一会让浏览器自动跳过转场的条件是文档的 page visibility state 为 hidden(窗口被遮挡、最小化或切到别的标签页);用户的减弱动效偏好不在其中。要尊重它,得自己写一条 media
query 把 ::view-transition-group(*) 一类伪元素上的动画关掉,或改用一次性的静态提示。
警示 · 同一时刻一个 view-transition-name 只能属于一个已渲染的元素。若两个元素同时挂了同一个名字,ViewTransition.ready 会 reject、整次转场被跳过——本页的 demo 用「隐藏另一侧」规避。跨文档场景下还有一个容易忽略的来源:bfcache
会把设过名字的页面状态一并存下,回退时旧名字与新名字撞车,所以 MDN 的示例在快照捕获完成后就把 view-transition-name 置回 none。
| 能力 | Chrome / Edge | Safari | Firefox |
|---|---|---|---|
同文档 document.startViewTransition() |
111 | 18 | 144 |
跨文档 @view-transition |
126 | 18.2 | 尚未支持(bug 1860854) |
两条线的进度差了整整一档(核对于 2026-08):同文档转场自 Firefox 144(2025 年 10 月)起三家齐备,可以按常规能力用;跨文档仍是 Chromium 与 WebKit 两家,落到 Firefox 上会静默退化成硬切——这正是它值得用的地方,缺了动画并不影响功能。