Web 平台 API / ✦ UX 交互模式 · 那些有名字的交互细节 / View Transitions:让同一个元素在两个状态间「长」过去 待审核 7 / 7
view-transition · 共享元素转场

View Transitions:让同一个元素在两个状态间「长」过去

从一个缩略图点进详情页,如果界面硬切,用户得在新画面里重新找回「我刚点的是哪个」。共享元素转场 (Shared Element Transition) 的思路是告诉浏览器前后两个状态里哪个元素是同一个,剩下的位移与缩放中间帧由它自动补出——缩略图会平滑地长成大图,视线被这条连续的运动一路带过去。原生 document.startViewTransition() 让这件事不再需要手写动画。

图 0-1 · 同一次列表到详情的切换,开与关转场的并排对照。可反复开合,观察缩略图是长成大图还是直接被替换。

机制只有两步:给缩略图和详情大图标上相同的 view-transition-name;把切换 DOM 的操作包进 startViewTransition(callback)。浏览器在回调前捕获旧状态、回调后捕获新状态,对同名元素自动生成 transformclip 的补间,其余内容默认做交叉淡入淡出。整个过程不写一行 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

图 1-1 · 内嵌双页站点的真实跨文档导航。右上角可在默认 crossfade 与圆形 clip-path 擦除之间切换整页效果。

转场期间浏览器把捕获结果拆成一棵伪元素树:每个 view-transition-name 各占一个 group,旧、新两帧叠在 image-pair 里交叉淡入淡出。想定制哪段动画,就用 @keyframes 命中对应伪元素。

图 1-2 · ::view-transition-* 伪元素树的层级。可点各层看它对应页面上的哪一块。

把整页 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

表 1-1 · 两种转场的支持情况,起始版本号(核对于 2026-08)。
能力 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 上会静默退化成硬切——这正是它值得用的地方,缺了动画并不影响功能。