动画与交互体验

好的动画不是装饰——它传达空间关系、引导注意力、提供操作反馈。但过度动画会让应用变得浮夸且缓慢。本章讲解 Next.js 中的动画技术栈选型、View Transitions API、加载状态设计、微交互实现,以及如何在性能和体验之间找到平衡。

1. 动画基础原则

1.1 动画的目的

目的示例重要性
空间关系页面切换的滑动方向🔴 核心
操作反馈按钮点击后的状态变化🔴 核心
引导注意新元素出现的淡入🟡 重要
状态变化展开/折叠、开/关🟡 重要
等待过渡骨架屏、加载指示器🟡 重要
品牌感独特的过渡效果🟢 锦上添花

1.2 动画的性能模型

浏览器渲染管道:

JS → Style → Layout → Paint → Composite
触发阶段属性性能
Composite onlytransform, opacity⚡ 最佳(GPU 加速)
Paintcolor, background, box-shadow🟡 中等
Layoutwidth, height, margin, padding🔴 最差(回流)

核心原则:只动画 transformopacity——它们可以直接在 GPU 上完成,不触发 Layout 和 Paint。

1.3 动画时长参考

动画类型推荐时长缓动函数
微交互(hover、click)100-150msease-out
元素进入/退出200-300msease-out
页面切换300-500msease-in-out
大面积展开/折叠300-400msease-in-out
弹窗/抽屉200-300msease-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/CSSFramer Motion
简单过渡
进入/退出动画⚠️ 有限AnimatePresence
弹簧物理
布局动画layout prop
手势交互drag, whileHover
动画编排stagger, when
Shared LayoutlayoutId
Server ComponentsN/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 gzippedmotion/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 transition0KB简单过渡

建议:先用 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-DocumentCross-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-dropAtlassian 出品,轻量~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

本章小结

  • 核心原则:只动画 transformopacity,100-300ms 时长,ease-out 缓动
  • CSS 优先:简单交互用 Tailwind transition,@starting-style 解决进入动画
  • Framer Motion:复杂场景的首选——AnimatePresencelayout、弹簧物理,注意包体积
  • View Transitions:浏览器原生页面切换动画,渐进增强,Chrome/Safari 已支持
  • 加载状态:骨架屏 + 独立 Suspense 边界,避免 Loading 瀑布
  • 微交互:hover/click/滚动/拖拽,每个都有最适合的实现方式
  • 无障碍:尊重 prefers-reduced-motion,动画不是信息的唯一传达方式
  • 选型:CSS → motion/react → framer-motion,按需升级复杂度