
路由一切就白一下、整页重绘,这种体验在 2026 年已经说不过去了。用户从列表点进详情,眼睛还在找同一张图,页面却先把自己拆掉再重装。动画库能补,但往往为了“顺滑”多背几 KB 甚至几十 KB,还要自己对接 App Router 的导航生命周期。
React 的 <ViewTransition> 和 Next.js 文档里的 View Transitions 指南,把这件事往浏览器原生能力上收了一截。名字对上了,浏览器负责补间;路由本身就是 transition,不用再手写一套 document.startViewTransition 胶水。
我这周在两个内部后台试了共享元素过渡。结论先说:列表到详情的“同一张图飞过去”很好用;想靠它重做整站电影级转场,还是会踩坑。
它到底接在哪一层
浏览器的 View Transitions API 干的事很朴素:在 DOM 状态 A 和 B 之间拍两张快照,用 CSS 做过渡。React 的 <ViewTransition> 负责在 React 更新周期里声明“哪些节点是同一身份”。Next.js App Router 的导航已经走 transition,所以路由切换时组件会自动参与,不必在 Link 上再包一层启动逻辑。
官方写法大致是这样:
import { ViewTransition } from 'react'
import Link from 'next/link'
import Image from 'next/image'
export function PhotoCard({ photo }: { photo: Photo }) {
return (
<Link href={`/photo/${photo.id}`}>
<ViewTransition name={`photo-${photo.id}`}>
<Image src={photo.src} alt={photo.title} width={320} height={240} />
</ViewTransition>
</Link>
)
}
// app/photo/[id]/page.tsx
export default function PhotoPage({ photo }: { photo: Photo }) {
return (
<ViewTransition name={`photo-${photo.id}`}>
<Image src={photo.src} alt={photo.title} width={960} height={720} priority />
</ViewTransition>
)
}
两边 name 一致,浏览器就把缩略图和主图当成同一元素做 morph。默认动画大约四分之一秒的交叉淡入,用 ::view-transition-old(*) / ::view-transition-new(*) 可以改手感。
文档还提醒:普通的 setState 不会触发;要挂在 Transition、Suspense 边界或 useDeferredValue 这类更新上。路由导航刚好属于前者,所以“点 Link 就动”是预期行为,不是魔法。
先做共享元素,别一上来全站 fade
最容易交付、也最不容易翻车的,是共享元素,不是 root 整页淡入淡出。整页 fade 在慢网或大数据页面上会显得拖,还和流式渲染抢注意力:内容还在往下淌,外壳已经在播过渡,用户会觉得“动完了怎么还在加载”。
我建议的落地顺序:
- 只给列表卡和详情主视觉加同一
name - 用 CSS 压默认时长到 180–220ms,缓动别花
- 检查
prefers-reduced-motion - 再考虑侧栏、标题这类次要元素要不要跟
@media (prefers-reduced-motion: no-preference) {
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 180ms;
}
}
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
name 必须稳定且页面内唯一。用数据库 id,别用数组下标。列表虚拟滚动时,滚出视口的节点如果卸掉了,过渡可能找不到旧快照,表现会退化成普通切换——这不是 bug 报告优先级最高的那种,但产品体感上会“有时有动画有时没有”。
图:列表缩略图与详情主视觉共用同一 transition name

和 Framer Motion 怎么分工
View Transitions 擅长跨路由、跨文档状态的“同一元素还在”。Framer Motion、GSAP 更擅长组件内部的手势、交错入场、拖拽回弹。2026 年比较清醒的拆法是:
- 路由级共享元素、列表→详情:优先 View Transitions
- 页内微交互、编排时间线:继续用动画库或纯 CSS
- 不要为了“统一技术方案”强行用一种工具打所有动画
包体上,原生路径几乎是零依赖。团队若已经为进场动画付了 Motion 的成本,也没必要为了时髦拆掉;新项目或性能敏感页,可以先只上 View Transitions,看设计是否真的需要更重的编排。
Next.js 文档:View transitions。React 侧仍要盯 canary / 稳定版通道差异,升依赖前在目标浏览器矩阵里走一遍真实导航,别只看文档示例的 gallery demo。
和 RSC、流式别互相踩脚
上一篇写过:壳被顶层 await 堵住时,流式等于没开。View Transition 叠在上面会更尴尬——旧页已经开始 capture,新页 HTML 却迟迟不齐,过渡要么拉长,要么落到不完整树。
实用约束:
- 参与 morph 的图片尽量有明确尺寸,避免过渡中途布局抖动
- 详情页 hero 用
priority或预加载,减少“先糊一层再换成真图” - 慢数据仍放
Suspense子树,不要为了动画把整页等齐 - 测量时看 INP 和过渡期间的长任务,不只看“好不好看”
浏览器支持也要心里有数。Chromium 系最齐,Safari/Firefox 在跟,但企业内网或旧 WebView 仍可能直接无动画降级。降级必须是“瞬间切换仍可用”,不能是“半截 DOM + 空白”。
调试时我实际怎么查
Chrome 里把 Animations 面板打开,导航一次,看 view transition 的 group 是否生成、时长是否被 CSS 拉爆。React 树里用临时边框或 outline 标出带 name 的节点,确认列表页和详情页在过渡瞬间确实同时存在于新旧快照里。
假动画的常见原因就三条:name 拼错或不稳定;新页图片还没解码,morph 对的是占位框;过渡触发时路由被重定向打断,旧树已经卸了。第三种在带权限校验的后台很常见——先跳登录再回来,共享元素故事直接结束。
若框架版本仍把 API 放在 experimental / canary,锁版本写进 AGENTS.md 或内部 runbook,避免 coding agent 升依赖时把过渡行为默默升坏。
小结
View Transitions 不是新的动画框架,是浏览器给的跨状态补间,React/Next 把它接到了组件树和路由上。先做一处共享元素,压时长、尊重减少动态效果,再谈全站设计语言。动画库该留的留,该减的减。用户记住的通常不是缓动曲线名字,而是“点进去的还是刚才那张图”。


