CSS动画基础:用transition和keyframes创造流畅交互
好的动画是无形的——用户不会说「这个动画很好」,他们只会说「这个应用用起来很舒服」。掌握 CSS 动画,是连通界面与直觉的必修课。
2008 年我刚入行时,Flash 还活着(尽管已经在喘息)。现在回想起来,Flash 教会了整整一代设计师一件 CSS 后来也学会了的事:动画不是装饰。动画是信息在时间维度上的编排。
这篇文章的目标读者是已经能写出静态页面、但想系统掌握 Web 动画的前端开发者。我不会罗列所有 CSS 动画属性——MDN 已经做得很好了。我要写的是实战中的心智模型、性能陷阱和可维护模式。
Section 01动画的本质:不是“动”,而是“变化被看见”
页面中的变化本来一直都在发生:按钮状态变了、卡片出现了、列表被排序了、内容被插入了。动画的作用不是制造变化,而是让变化变得可理解。
如果一切变化都瞬间完成,用户会失去空间定位和因果理解;如果变化太慢,用户会觉得系统在拖延。动画的价值就在这两者之间。
动画服务于认知
一个好的界面动画,通常在做四件事:
- 指示层级变化
- 维持空间连续性
- 引导注意力
- 提供反馈
也就是说,动画不是为了“好看”,而是为了减少用户理解页面变化时的心理成本。
Section 02Transition:状态间的桥梁
transition 是动画世界最简单、也最高频使用的工具。它处理的是状态间的自动补间:一个元素从 A 状态到 B 状态需要经过多少时间、走什么曲线。
四要素
| 1 | .button { |
| 2 | background: var(--color-accent); |
| 3 | transform: scale(1); |
| 4 | transition: background 0.2s ease-out, |
| 5 | transform 0.15s ease-out; |
| 6 | } |
| 7 | |
| 8 | .button:hover { |
| 9 | background: var(--color-accent-hover); |
| 10 | transform: scale(1.02); |
| 11 | } |
- property:要过渡的属性。尽量指定具体属性,不用
all——性能更好,意外更少 - duration:时长。交互反馈推荐 100-300ms,超过 500ms 用户会开始等
- timing-function:缓动函数。这是动画的灵魂
- delay:延迟。极少在交互动画中使用,但在编排(stagger)中是关键
缓动函数的心理效应
| 缓动 | 物理感觉 | 最佳场景 |
|---|---|---|
ease-out | 物体在减速 | 元素出现、展开 |
ease-in | 物体在加速 | 元素消失、关闭 |
ease-in-out | 对称的加速/减速 | 循环动画、缩放 |
linear | 匀速——机械 | 加载指示器、旋转 |
| 自定义 cubic-bezier | 品牌个性 | 界面签名动画 |
| 1 | /* 品牌弹跳——只适合特殊强调 */ |
| 2 | --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); |
| 3 | |
| 4 | /* 高质量通用缓出 */ |
| 5 | --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); |
| 6 | |
| 7 | /* Apple 风格的减速感 */ |
| 8 | --ease-apple: cubic-bezier(0.25, 0.1, 0.25, 1); |
Section 03@keyframes:编排时间轴
当动画超越了「从 A 到 B」而需要多阶段表达时,@keyframes 登场:
| 1 | @keyframes fadeInUp { |
| 2 | 0% { |
| 3 | opacity: 0; |
| 4 | transform: translateY(16px); |
| 5 | } |
| 6 | 100% { |
| 7 | opacity: 1; |
| 8 | transform: translateY(0); |
| 9 | } |
| 10 | } |
| 11 | |
| 12 | .card { |
| 13 | animation: fadeInUp 0.5s ease-out both; |
| 14 | } |
animation 属性速查
| 1 | .card { |
| 2 | animation-name: fadeInUp; |
| 3 | animation-duration: 0.5s; |
| 4 | animation-timing-function: ease-out; |
| 5 | animation-delay: 0.1s; |
| 6 | animation-iteration-count: 1; |
| 7 | animation-direction: normal; |
| 8 | animation-fill-mode: both; |
| 9 | } |
animation-fill-mode: both 是高频使用的设置:元素在动画开始前保持第一帧样式,动画结束后保持最后一帧。它在避免「闪烁」方面至关重要。
Section 04编排(Stagger):动画的音乐性
一个页面中的多个元素不应该同时入场。它们应该像音符一样有先后秩序:
| 1 | .card:nth-child(1) { animation-delay: 0ms; } |
| 2 | .card:nth-child(2) { animation-delay: 80ms; } |
| 3 | .card:nth-child(3) { animation-delay: 160ms; } |
| 4 | .card:nth-child(4) { animation-delay: 240ms; } |
Tailwind 中可以直接写入 inline style 或使用自定义 delay utilities。本博客文章列表的入场动画就使用了这个模式,每张卡延迟 80ms。
编排的黄金法则:总延迟不应超过 300ms——从第一个元素到最后一个元素的入场间隔如果太长,用户会产生「这个页面还在加载」的焦虑。
Section 05性能:只动画 transform 和 opacity
浏览器渲染管道由四个阶段组成:样式 → 布局 → 绘制 → 合成。
| 1 | Layout → 改变 width / height / left / top |
| 2 | Paint → 改变 color / background / box-shadow |
| 3 | Composite → 改变 transform / opacity |
每一帧(16.7ms @ 60fps)中,Layout 和 Paint 是昂贵的;Composite 是廉价的。因此:
| 1 | /* ❌ 触发布局和绘制 */ |
| 2 | @keyframes bad-move { |
| 3 | from { left: 0; top: 0; background: red; } |
| 4 | to { left: 100px; top: 50px; background: blue; } |
| 5 | } |
| 6 | |
| 7 | /* ✅ 仅触发合成 */ |
| 8 | @keyframes good-move { |
| 9 | from { transform: translate(0, 0); opacity: 0.5; } |
| 10 | to { transform: translate(100px, 50px); opacity: 1; } |
| 11 | } |
will-change 的正确用法
will-change 提前告诉浏览器「这个元素将发生变化」,让浏览器预先创建合成层:
| 1 | .slide-panel { |
| 2 | will-change: transform; |
| 3 | transition: transform 0.3s ease-out; |
| 4 | } |
注意:will-change 应该仅用于即将发生动画的元素,并且在动画结束后移除。滥用它会让浏览器为从未动画的元素预分配 GPU 内存。
prefers-reduced-motion
任何动画都应尊重用户的系统级偏好:
| 1 | @media (prefers-reduced-motion: reduce) { |
| 2 | *, |
| 3 | *::before, |
| 4 | *::after { |
| 5 | animation-duration: 0.01ms !important; |
| 6 | transition-duration: 0.01ms !important; |
| 7 | } |
| 8 | } |
这不是「禁用」动画——而是将动画时间压缩到几乎不可见,保留操作性的动画(如焦点环、页面切换),同时去除纯装饰性的动画。
Section 06动画设计中最常见的几个误区
1. 把动画做成装饰
如果动画没有提供层级、反馈或方向感,它通常只是噪音。
2. 把所有动效做得一样
大按钮和小图标、页面切换和 hover 反馈,它们的目标不同,时间感也不应该一样。
3. 忽略可访问性
对某些用户来说,强烈的位移、闪烁或持续循环会带来不适。prefers-reduced-motion 不是可选项。
4. 只盯着单个元素
真正的动画设计要关注的是整个界面的节奏,不只是某个元素单独好不好看。
Section 07实战场景
按钮点击反馈
| 1 | .button:active { |
| 2 | transform: scale(0.96); |
| 3 | transition: transform 0.1s ease; |
| 4 | } |
页面加载编排
| 1 | .hero-title { animation: fadeInUp 0.6s ease-out both; } |
| 2 | .hero-text { animation: fadeInUp 0.6s ease-out 0.1s both; } |
| 3 | .hero-cta { animation: fadeInUp 0.6s ease-out 0.2s both; } |
卡片 hover 提升
| 1 | .card { |
| 2 | transform: translateY(0); |
| 3 | box-shadow: 0 2px 8px rgba(0,0,0,0.1); |
| 4 | transition: transform 0.25s ease-out, |
| 5 | box-shadow 0.25s ease-out; |
| 6 | } |
| 7 | |
| 8 | .card:hover { |
| 9 | transform: translateY(-4px); |
| 10 | box-shadow: 0 12px 30px rgba(0,0,0,0.15); |
| 11 | } |
页面切换的视觉连续性
如果一个页面从列表切换到详情页,最好的体验不是“跳转成功”,而是“我刚刚点的那张卡变成了详情页”。这种连续性会极大降低认知成本。
Section 08结语
动画是时间轴上的排版。和字体排印一样,它是一门可以被学习、被演练、被精通的工艺。它不是一个「会动就行」的 afterthought——它是你的界面在时间维度上的完整表达。