动画与交互体验
好的动画不是装饰——它传达空间关系、引导注意力、提供操作反馈。但过度动画会让应用变得浮夸且缓慢。本章讲解 Next.js 中的动画技术栈选型、View Transitions API、加载状态设计、微交互实现,以及如何在性能和体验之间找到平衡。
1. 动画基础原则
1.1 动画的目的
| 目的 | 示例 | 重要性 |
|---|
| 空间关系 | 页面切换的滑动方向 | 🔴 核心 |
| 操作反馈 | 按钮点击后的状态变化 | 🔴 核心 |
| 引导注意 | 新元素出现的淡入 | 🟡 重要 |
| 状态变化 | 展开/折叠、开/关 | 🟡 重要 |
| 等待过渡 | 骨架屏、加载指示器 | 🟡 重要 |
| 品牌感 | 独特的过渡效果 | 🟢 锦上添花 |
1.2 动画的性能模型
浏览器渲染管道:
JS → Style → Layout → Paint → Composite
| 触发阶段 | 属性 | 性能 |
|---|
| Composite only | transform, opacity | ⚡ 最佳(GPU 加速) |
| Paint | color, background, box-shadow | 🟡 中等 |
| Layout | width, height, margin, padding | 🔴 最差(回流) |
核心原则:只动画 transform 和 opacity——它们可以直接在 GPU 上完成,不触发 Layout 和 Paint。
1.3 动画时长参考
| 动画类型 | 推荐时长 | 缓动函数 |
|---|
| 微交互(hover、click) | 100-150ms | ease-out |
| 元素进入/退出 | 200-300ms | ease-out |
| 页面切换 | 300-500ms | ease-in-out |
| 大面积展开/折叠 | 300-400ms | ease-in-out |
| 弹窗/抽屉 | 200-300ms | ease-out |
快于 100ms:用户感觉不到动画
慢于 500ms:用户感觉卡顿
2. CSS 动画
2.1 何时使用纯 CSS
| ✅ 适合 CSS 的场景 | ❌ 需要 JS 库的场景 |
|---|
| 简单的 hover/focus 效果 | 基于手势的交互 |
| 进入/退出动画 | 弹簧物理动画 |
| 循环动画(spinner) | 编排复杂的动画序列 |
@keyframes 动画 | 拖拽排序动画 |
视差滚动(scroll-timeline) | 路由切换动画 |
2.2 CSS 原生动画能力(2025)
| API | 支持度 | 用途 |
|---|
transition | ✅ 全支持 | 属性变化过渡 |
@keyframes | ✅ 全支持 | 自定义动画序列 |
animation-timeline: scroll() | ✅ Chrome/Edge | 滚动驱动动画 |
animation-timeline: view() | ✅ Chrome/Edge | 元素可见性驱动 |
@starting-style | ✅ Chrome 117+ | 元素进入动画 |
transition-behavior: allow-discrete | ✅ Chrome 117+ | display: none 过渡 |
| View Transitions API | ✅ Chrome 111+ | 页面切换动画 |
2.3 @starting-style:CSS 进入动画
解决了一个长期痛点——纯 CSS 实现元素从 display: none 到可见的过渡:
之前(需要 JS):
1. 设置 display: block
2. 等待一帧
3. 添加 .visible class 触发过渡
现在(纯 CSS):
@starting-style {
.dialog { opacity: 0; transform: scale(0.95); }
}
.dialog { opacity: 1; transform: scale(1); transition: all 0.2s; }
3. Framer Motion
3.1 为什么选择 Framer Motion
| 能力 | Tailwind/CSS | Framer Motion |
|---|
| 简单过渡 | ✅ | ✅ |
| 进入/退出动画 | ⚠️ 有限 | ✅ AnimatePresence |
| 弹簧物理 | ❌ | ✅ |
| 布局动画 | ❌ | ✅ layout prop |
| 手势交互 | ❌ | ✅ drag, whileHover |
| 动画编排 | ❌ | ✅ stagger, when |
| Shared Layout | ❌ | ✅ layoutId |
| Server Components | N/A | ❌ 需要 'use client' |
3.2 Framer Motion 的核心概念
| 概念 | 说明 |
|---|
motion.div | 可动画的 HTML 元素 |
initial | 初始状态 |
animate | 目标状态 |
exit | 退出状态(需要 AnimatePresence) |
transition | 动画配置(时长、缓动、弹簧参数) |
variants | 预定义的状态集 |
layout | 自动布局动画 |
layoutId | 跨组件共享布局动画 |
3.3 Framer Motion 在 Next.js 中的注意事项
| 问题 | 原因 | 解决方案 |
|---|
| 包体积大 | ~30KB gzipped | 用 motion/react 的 tree-shaking |
| 需要 Client Component | 依赖 React state/effect | 将动画组件标记为 'use client' |
| SSR 闪烁 | initial 状态在服务端渲染 | 设置合理的 initial 或禁用 |
| 与 Server Components 不兼容 | 动画逻辑是客户端逻辑 | 只在叶子组件用 motion |
3.4 motion/react 轻量版
2025 年 Framer Motion 推出了 motion/react——更小的包体积:
| 包 | 大小 | 能力 |
|---|
framer-motion | ~30KB | 全功能 |
motion/react | ~15KB | 核心功能(够用) |
CSS transition | 0KB | 简单过渡 |
建议:先用 CSS transition,不够用再上 motion/react,极少数复杂场景才用完整的 framer-motion。
4. View Transitions API
4.1 什么是 View Transitions
View Transitions API 是浏览器原生的页面切换动画——在两个 DOM 状态之间自动生成过渡效果:
旧页面状态 → 截图
↓
DOM 更新
↓
新页面状态 → 截图
↓
浏览器自动在两个截图之间做交叉淡入淡出
关键优势:不需要 JS 动画库,浏览器在 Compositor 层面做动画——性能极佳。
4.2 Next.js 中的 View Transitions
| 方式 | 支持 | 说明 |
|---|
| Next.js 内置 | ⚠️ 实验性 | next.config.js 中启用 |
next-view-transitions | ✅ | 社区库,API 更友好 |
| 手动实现 | ✅ | 直接调用浏览器 API |
4.3 View Transitions 的使用场景
| 场景 | 效果 |
|---|
| 页面导航 | 交叉淡入淡出 |
| 列表 → 详情 | 元素展开过渡 |
| 标签切换 | 内容滑动 |
| 图片画廊 | 图片放大/缩小 |
4.4 浏览器支持
| 浏览器 | Same-Document | Cross-Document |
|---|
| Chrome 111+ | ✅ | ✅(Chrome 126+) |
| Safari 18+ | ✅ | ✅ |
| Firefox | ⚠️ 开发中 | ❌ |
渐进增强策略:View Transitions 不支持的浏览器直接跳过动画——用户看到的是正常的页面切换,不会出错。
5. 加载状态设计
5.1 加载状态类型
| 类型 | 适用场景 | 实现 |
|---|
| 骨架屏(Skeleton) | 页面/列表首次加载 | Suspense fallback |
| 行内 Spinner | 按钮提交中 | CSS animation |
| 进度条 | 文件上传/长操作 | NProgress / 自定义 |
| 全屏 Loading | 页面切换(慎用) | 路由 loading.tsx |
| 乐观更新 | 用户操作后立即反馈 | useOptimistic |
| 占位符 | 图片/内容加载 | blur placeholder |
5.2 骨架屏设计原则
| 原则 | 说明 |
|---|
| 布局匹配 | 骨架屏的布局应该和最终内容一致 |
| 脉冲动画 | 使用 animate-pulse 表示加载中 |
| 避免过于具体 | 不需要精确模拟每个元素 |
| 快速消失 | 数据到达后立即替换,不要有额外过渡 |
| 不要闪烁 | 数据极快返回时不要显示骨架屏(最小显示时间) |
5.3 Next.js 的 loading.tsx
App Router 的 loading.tsx 是路由级别的 Suspense 边界:
app/
├── dashboard/
│ ├── loading.tsx ← 路由加载时显示
│ └── page.tsx ← 实际页面内容
层级关系:
layout.tsx(始终显示)
├── loading.tsx(加载中显示)← Suspense boundary
└── page.tsx(加载完成显示)
5.4 避免 Loading 瀑布
❌ 瀑布式加载:
加载 Layout → 加载 Sidebar → 加载 Content → 加载 Comments
总时间:400ms + 300ms + 500ms + 200ms = 1400ms
✅ 并行加载 + 独立 Suspense:
Layout 立即渲染
├── Sidebar(独立 Suspense)→ 300ms
├── Content(独立 Suspense)→ 500ms
└── Comments(独立 Suspense)→ 200ms
总时间:500ms(最慢的那个)
6. 微交互设计
6.1 常见微交互
| 交互 | 动画 | 实现 |
|---|
| 按钮 hover | 背景色变化 | CSS transition-colors |
| 按钮点击 | 缩小反弹 | CSS active:scale-95 |
| 链接 hover | 下划线滑入 | CSS ::after + scaleX |
| 复选框 | 打勾动画 | SVG stroke-dashoffset |
| 开关 | 滑块移动 | CSS translate |
| 通知 | 从右侧滑入 | Framer Motion AnimatePresence |
| 手风琴 | 高度展开 | CSS grid-template-rows |
| 数字变化 | 数字翻滚 | Framer Motion AnimatePresence |
6.2 滚动动画
| 方式 | 性能 | 复杂度 | 适用 |
|---|
CSS scroll-timeline | ⚡ 最佳 | 低 | 简单视差 |
| Intersection Observer | ✅ 好 | 中 | 元素进入可视区 |
Framer Motion useScroll | ✅ 好 | 中 | 复杂滚动联动 |
监听 scroll 事件 | ❌ 差 | 低 | 不推荐 |
Intersection Observer 是实现"元素进入可视区时播放动画"的最佳方式——不阻塞主线程,性能优秀。
6.3 拖拽交互
| 库 | 特点 | 大小 |
|---|
@dnd-kit/core | 可访问性好,模块化 | ~10KB |
framer-motion drag | 物理感、流畅 | 包含在 framer-motion 中 |
react-beautiful-dnd | 经典,但已停止维护 | ~30KB |
pragmatic-drag-and-drop | Atlassian 出品,轻量 | ~5KB |
7. 无障碍与动画
7.1 prefers-reduced-motion
部分用户对动画敏感(前庭障碍),浏览器提供了 prefers-reduced-motion 媒体查询:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
Tailwind v4 提供了 motion-reduce: 变体:motion-reduce:transition-none。
7.2 动画无障碍清单
| 检查项 | 说明 |
|---|
✅ 尊重 prefers-reduced-motion | 减少/移除动画 |
| ✅ 不依赖动画传达信息 | 动画只是增强,不是唯一线索 |
| ✅ 避免闪烁 | 每秒闪烁 > 3 次可能诱发癫痫 |
| ✅ 可暂停 | 自动播放的动画应该可暂停 |
| ✅ 焦点管理 | 动画完成后焦点在正确位置 |
8. 动画选型决策树
需要动画吗?
├── 简单过渡(hover/focus/颜色变化)
│ → CSS transition(Tailwind 工具类)
├── 进入/退出动画
│ ├── 简单淡入淡出 → CSS @starting-style
│ └── 复杂编排 → Framer Motion AnimatePresence
├── 布局动画
│ → Framer Motion layout
├── 页面切换
│ → View Transitions API
├── 滚动驱动
│ ├── 简单视差 → CSS scroll-timeline
│ └── 复杂联动 → Intersection Observer / useScroll
├── 手势/拖拽
│ → @dnd-kit 或 Framer Motion drag
└── 物理弹簧
→ Framer Motion spring
本章小结
- 核心原则:只动画
transform 和 opacity,100-300ms 时长,ease-out 缓动
- CSS 优先:简单交互用 Tailwind transition,
@starting-style 解决进入动画
- Framer Motion:复杂场景的首选——
AnimatePresence、layout、弹簧物理,注意包体积
- View Transitions:浏览器原生页面切换动画,渐进增强,Chrome/Safari 已支持
- 加载状态:骨架屏 + 独立 Suspense 边界,避免 Loading 瀑布
- 微交互:hover/click/滚动/拖拽,每个都有最适合的实现方式
- 无障碍:尊重
prefers-reduced-motion,动画不是信息的唯一传达方式
- 选型:CSS → motion/react → framer-motion,按需升级复杂度