起因
SPA 路由切换 instant 但生硬:
- click link → 内容直接换
- 没有 native app 那种"页面滑入"感觉
要做平滑过渡过去要用 framer-motion / GSAP / react-transition-group 等,
JS 重 + 配置 + 难做"shared element transition"(同一元素跨页面渐变到
新位置)。
View Transitions API(Chrome 111+,Safari 18+)让浏览器原生做这事:
document.startViewTransition(() => {
updateDOM(); // 你只管 update DOM
});
// 浏览器自动 fade(默认)
3 行启用,CSS 控制效果。
基本 fade
function navigate(url) {
if (!document.startViewTransition) {
return loadPage(url); // fallback
}
document.startViewTransition(() => loadPage(url));
}
浏览器拍 snapshot → DOM 改 → 自动 crossfade。
自定义动画
CSS:
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 300ms;
}
::view-transition-old(root) {
animation: slide-out 300ms;
}
::view-transition-new(root) {
animation: slide-in 300ms;
}
@keyframes slide-out {
to { transform: translateX(-100%); }
}
@keyframes slide-in {
from { transform: translateX(100%); }
}
浏览器把"老" 和"新" 都当独立的 pseudo-element animate。
slide 效果。
shared element transition
同一元素从 page A 滑到 page B 新位置:
/* page A */
.hero-image { view-transition-name: hero; }
/* page B */
.detail-image { view-transition-name: hero; }
view-transition-name 相同的两个元素 → 浏览器自动 morph 过去(位置 +
大小)。
效果:list 页 thumb 点击后展开到 detail 页大图的位置 → 流畅滑动。
native app 这是常见效果,web 历来很难做。
跨文档 (MPA) 也行
/* old behavior: SPA only */
@view-transition {
navigation: auto;
}
加这个 → 普通 <a href> 跳转也自动有 view transition。
MPA / 服务端渲染网站直接得到 SPA 般体验。
Chrome 126+ 支持,Safari 18+。
实战 example:image gallery
<!-- list page -->
<ul>
<li><a href="/photo/1"><img src="thumb1.jpg" class="thumb-1"></a></li>
<li><a href="/photo/2"><img src="thumb2.jpg" class="thumb-2"></a></li>
</ul>
<!-- detail page /photo/1 -->
<img src="full1.jpg" class="thumb-1">
.thumb-1 { view-transition-name: photo-1; }
.thumb-2 { view-transition-name: photo-2; }
@view-transition { navigation: auto; }
点 list 缩略图 → 滑到 detail 页大图位置(同 view-transition-name
morph)。
50 行 CSS / 0 行 JS framework 出 native app 体验。
SPA framework 集成
SvelteKit:
// app.html / hook
import { onNavigate } from '$app/navigation';
onNavigate((navigation) => {
if (!document.startViewTransition) return;
return new Promise((resolve) => {
document.startViewTransition(async () => {
resolve();
await navigation.complete;
});
});
});
Next.js (with App Router 14+) 计划 next-view-transitions package 集成。
Astro 4 内置 view transitions(最早接入的 SPA framework)。
自定义动画 token
::view-transition-group(*) {
animation-duration: 200ms;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
::view-transition-old(*) {
animation-name: fade-out;
}
::view-transition-new(*) {
animation-name: fade-in;
}
@keyframes fade-out { to { opacity: 0; } }
@keyframes fade-in { from { opacity: 0; } }
* 是所有 view-transition-name,统一应用。
reduce motion
@media (prefers-reduced-motion: reduce) {
::view-transition-old(*),
::view-transition-new(*) {
animation: none;
}
}
尊重无障碍偏好,关闭动画。
fallback
async function navigate(url) {
const update = () => loadPage(url);
if (document.startViewTransition) {
document.startViewTransition(update);
} else {
update();
}
}
老浏览器(Firefox 现在还不支持)→ 直接换不动画。
不需要 polyfill,平滑降级。
debug
Chrome DevTools 有 view transition debugger:
- Animation panel 看 frames
- Performance panel 看 transition cost
慢的话查:
- snapshot 渲染贵
- 动画太多并行
性能
view transition 是 GPU 加速(compositor 层),60 fps 容易。
比 React state 切换 + CSS transition 效率高。
但 snapshot 大 element(整页)也有成本。
500ms 不算 view transition 适合场景。
与 framer-motion 对比
| View Transitions API | framer-motion | |
|---|---|---|
| 配置 | CSS + 几行 JS | JS 重 |
| 大小 | 0 (原生) | ~30 KB |
| shared element | 简单 | 复杂 (AnimatePresence) |
| 兼容 | Chrome / Safari (Firefox no) | 全部 |
| 灵活度 | 中 | 极高 |
simple fade / slide / shared element → View Transitions API。
复杂交互(drag / gesture / spring physics)→ framer-motion。
真实 case:内容站
我们一个博客 + 文档站,加了 5 行 CSS(@view-transition: navigation: auto
+ hero 图 view-transition-name)。
效果:
- 点击文章卡片 → 卡片 morph 到文章详情 header
- 切换文档章节 → 内容 fade
- 滑动手感像 native app
- bundle 增加 0(纯 CSS / 浏览器原生)
- code 改动几十行
用户反馈:"站点感觉变快了"(实际加载没变,但平滑过渡心理 perceived
performance 提升)。
不适合的场景
- 复杂物理动画(spring / momentum)→ 用 framer / GSAP
- 需要 user gesture(drag / pinch)→ 用 framer
- 极致性能场景(动画 60fps + 5+ 大元素并发)
踩过的坑
-
view-transition-name必须 unique:两个元素同名同时存在 →
只 transition 一个。dynamic list 用 unique id 后缀。 -
width/height 变化 morph 怪:position absolute 大小变化时
transform 计算偏差。试width: auto与aspect-ratio配合。 -
dark mode 切换不平滑:toggle dark mode 时 view transition →
crossfade 整页。慢设备卡。考虑 disable。 -
图片 loading:transition 时新图还没加载 → 闪。preload 关键
img。 -
嵌套 transition:transition 期间 trigger 另一 transition →
undefined behavior。debounce / sequence。
登录后参与评论。