View Transitions API:用 CSS 给 SPA 加平滑过渡

起因

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+ 大元素并发)

踩过的坑

  1. view-transition-name 必须 unique:两个元素同名同时存在 →
    只 transition 一个。dynamic list 用 unique id 后缀。

  2. width/height 变化 morph 怪:position absolute 大小变化时
    transform 计算偏差。试 width: autoaspect-ratio 配合。

  3. dark mode 切换不平滑:toggle dark mode 时 view transition →
    crossfade 整页。慢设备卡。考虑 disable。

  4. 图片 loading:transition 时新图还没加载 → 闪。preload 关键
    img。

  5. 嵌套 transition:transition 期间 trigger 另一 transition →
    undefined behavior。debounce / sequence。

精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。