View Transitions 不是动画库
第一次看到 document.startViewTransition 的时候,我以为它就是个更简单的 GSAP。
用了半年之后我发现,它真正解决的问题根本不是"怎么动画"。
它解决的是什么问题
传统的页面转场动画难写,不是因为 CSS 难,而是因为你要同时管理两个状态。
从列表页跳到详情页,你得:
- 给旧页面加一个 "leaving" 类
- 等动画结束
- 卸载旧页面
- 挂载新页面
- 给新页面加一个 "entering" 类
- 再等动画结束
六个步骤,任何一步时序错一点就会闪。
View Transitions 把这六步变成了一个 API:
document.startViewTransition(() => {
// 只在这里改 DOM,浏览器负责前后的快照和过渡
renderNewPage()
})
浏览器会:对旧状态截图 → 执行你的回调 → 对新状态截图 → 在两帧之间做过渡。
你只描述"变成什么样",不描述"怎么变"。
三种实际用法
一、页面切换
Nuxt 开了 experimental.viewTransition 之后,页面过渡可以完全用 CSS 控制:
::view-transition-old(root) {
animation: 200ms ease-out both fade-out;
}
::view-transition-new(root) {
animation: 200ms ease-out both fade-in;
}
@keyframes fade-out {
to { opacity: 0; }
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(4px);
}
}
注意这里没有 JS。转场逻辑全在 CSS 里,JS 只负责换内容。
二、主题切换的圆形扩散
这个效果很多人以为是手写 clip-path 加 requestAnimationFrame,其实可以完全交给 View Transitions:
async function toggleTheme(x: number, y: number) {
const radius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y),
)
const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})
await transition.ready
document.documentElement.animate(
{
clipPath: [
`circle(0px at ${x}px ${y}px)`,
`circle(${radius}px at ${x}px ${y}px)`,
],
},
{
duration: 200,
easing: 'ease-out',
pseudoElement: '::view-transition-new(root)',
},
)
}
关键在 pseudoElement: '::view-transition-new(root)'——你不是在动画真实 DOM,是在动画浏览器的过渡快照层。所以这个动画不会触发重排。
三、列表到详情的共享元素
给元素一个 view-transition-name:
.post-card__title { view-transition-name: post-title; }
.post-detail__title { view-transition-name: post-title; }
两个页面上同名,浏览器就会把它们当成同一个元素,自动做位置和尺寸的插值。
::view-transition-old(post-title),
::view-transition-new(post-title) {
animation-duration: 250ms;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
必须知道的三个限制
一、同一时间只能有一个 view-transition-name 在页面上。
如果列表里有 10 张卡片都叫 post-title,浏览器会直接报错、整个过渡失效。所以列表页通常不给每张卡单独命名,只在用户点击时动态设置。
二、动画期间页面不可交互。
过渡快照覆盖在真实 DOM 之上。所以别做超过 300ms 的转场。
三、必须尊重 prefers-reduced-motion。
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches
const supported = 'startViewTransition' in document
if (!supported || reduceMotion) {
// 直接切,不做动画
applyChange()
return
}
这不是可选项。前庭功能障碍的用户会因为这种动画头晕。
什么时候不该用
- 表单提交后的状态更新 —— 不需要转场,用户只想知道成功了没
- 列表项的 hover 效果 —— 那是微交互,用
transition就够了 - 超过 300ms 的复杂编排 —— 会让人等
View Transitions 的定位是页面级的状态切换,不是"万能动画方案"。
一句话
它把"两套状态之间怎么过渡"这件麻烦事,从你手里拿走了。
这可能不是最酷的动画 API,但它是我用过最省心的那个。
评论暂未开放