CSS Motion 与动画原则
动画的基础原则
好的动画不是随意的——它们遵循时间(缓动)和空间(运动路径)的设计原则。错误的缓动函数或不恰当的时长会让动画显得廉价或不自然。
在 Web 开发中,动画不仅仅是视觉装饰,更是用户体验的重要组成部分。好的动画帮助用户理解界面变化、建立空间认知、提供操作反馈。不好的动画则会让用户感到困惑或烦躁。
理解动画原则,需要从物理世界和人脑感知两个角度来思考。现实世界中,物体运动遵循物理定律——有加速度、减速度、动量守恒。人的大脑也习惯了这种运动模式,当动画违背这些原则时,人会感到不自然。
面试定位:动画原则不是技术面试考点,但它是 UX/UI 设计的核心知识,体现前端工程师对用户体验的关注。面试官通过候选人对动画原则的理解、对缓动函数的掌握、以及实际应用场景的探索,来评估其用户体验意识和工程判断能力。
缓动函数
缓动函数(Easing Function)描述了动画随时间变化的速率。理解缓动函数,是创建自然动画的基础。
标准缓动函数
CSS 提供了一系列内置缓动函数:
.easing {
/* ease:慢快慢(默认值) */
animation-timing-function: ease;
/* linear:匀速 */
animation-timing-function: linear;
/* ease-in:慢开始 */
animation-timing-function: ease-in;
/* ease-out:慢结束 */
animation-timing-function: ease-out;
/* ease-in-out:慢开始和结束 */
animation-timing-function: ease-in-out;
}
缓动函数的可视化理解
ease: ___
/ \
/ \
/ \
/ \_______
linear: _______________
ease-in: /
/
/
/
/_______
ease-out: ____
/
/
/
/
ease-in-out: __
/ \
/ \
/ \
______/ \
为什么不用 linear
linear 看起来"公平",但实际上最不自然。因为现实世界中,物体运动总是有加速度的。线性动画会让物体运动看起来机械、僵硬,像机器人一样。
/* 线性动画 - 看起来机械 */
.bad {
animation: slide 1s linear;
}
/* ease 缓动 - 看起来自然 */
.good {
animation: slide 1s ease;
}
cubic-bezier 自定义缓动
cubic-bezier() 函数允许创建自定义的缓动曲线,这是实现高质量动画的关键。
语法
.custom-easing {
animation-timing-function: cubic-bezier(p1x, p1y, p2x, p2y);
}
四个参数定义了贝塞尔曲线的两个控制点。曲线的起点是 (0, 0),终点是 (1, 1)。
常用缓动曲线
/* Material Design 标准 */
.material {
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
/* Material Design 进入 */
.material-enter {
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
/* Material Design 退出 */
.material-exit {
animation-timing-function: cubic-bezier(0.4, 0, 1, 1);
}
/* iOS 标准 */
.ios {
animation-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
}
/* 强调效果 */
.emphasis {
animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
自定义缓动曲线工具
手动计算 cubic-bezier 比较困难,可以使用在线工具:
- cubic-bezier.com - 可视化贝塞尔曲线编辑器
- easings.net - 常用缓动函数参考
缓动函数的选择原则
不同的动画类型,应该选择不同的缓动函数。
进入 vs 退出动画
/* 进入动画:快速开始抓住注意力,然后自然减速停止 */
.enter {
animation-timing-function: ease-out; /* 或 cubic-bezier(0, 0, 0.2, 1) */
animation: fade-in 300ms ease-out forwards;
}
/* 退出动画:快速开始,然后继续加速离开 */
.exit {
animation-timing-function: ease-in; /* 或 cubic-bezier(0.4, 0, 1, 1) */
animation: fade-out 200ms ease-in forwards;
}
缓动选择指南
| 动画类型 | 缓动函数 | 原因 |
|---|---|---|
| 进入动画 | ease-out | 快速开始抓住注意力,然后自然减速停止 |
| 退出动画 | ease-in | 快速开始,然后继续加速离开,不拖沓 |
| 强调动画 | ease-in-out | 慢开始和慢结束更有节奏感,适合需要引起注意的效果 |
| 状态切换 | ease | 平衡的过渡 |
| 加载动画 | linear | 匀速旋转 |
物理模拟的缓动
有时候使用模拟物理效果的缓动更自然:
/* 弹性效果(overshoot) */
.elastic {
animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
/* 弹性消失 */
.elastic-out {
animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
动画时长
时长(Duration)是动画的另一个关键参数。过长会让用户等待,过短会让用户无法感知。
时长选择指南
.duration {
/* 微交互:100-300ms */
/* 适合:hover、点击反馈、按钮状态 */
transition: transform 150ms ease;
/* 状态切换:200-500ms */
/* 适合:展开/收起、下拉菜单、模态框 */
animation: fadeIn 300ms ease-out;
/* 复杂动画:400-1000ms */
/* 适合:页面过渡、列表重排 */
animation: slideIn 500ms cubic-bezier(0.4, 0, 0.2, 1);
}
时长原则
- < 100ms:用户感觉即时,不需要动画
- 100-300ms:适合微交互(hover、点击反馈)
- 300-500ms:适合状态切换(展开/收起)
- 500ms-1s:适合复杂动画,需要有明确的视觉焦点
- > 1s:通常过长,除非是叙事性动画
动画时长与缓动的配合
较短的动画应该使用较"急"的缓动(如 linear),较长的动画应该使用较"平缓"的缓动(如 ease-out):
/* 快速微交互:短时间 + linear */
.quick {
transition: transform 100ms linear;
}
/* 状态切换:中等时间 + ease-out */
.state-change {
transition: transform 300ms ease-out;
}
/* 页面过渡:较长时间 + 精心设计的缓动 */
.page-transition {
animation: slide 500ms cubic-bezier(0.4, 0, 0.2, 1);
}
动画性能优化
GPU 加速属性
以下属性可以利用 GPU 加速,性能最好:
.optimized {
/* transform 变换 */
transform: translateX(100px);
transform: scale(1.5);
transform: rotate(45deg);
/* opacity 透明度 */
opacity: 0.5;
/* filter 滤镜(部分) */
filter: blur(2px);
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
}
应避免的属性
以下属性会触发重排或重绘,性能较差:
.avoid {
/* 触发重排的属性 */
width: 100px;
height: 100px;
margin: 10px;
padding: 10px;
top: 10px;
left: 10px;
/* 触发重绘的属性 */
background-color: red;
color: blue;
border-color: green;
}
will-change 提示
will-change 告诉浏览器元素即将变化,帮助浏览器提前优化:
.will-change {
/* 提前声明将要变化的属性 */
will-change: transform, opacity;
/* 避免过度使用 */
/* 不要这样:will-change: all; */
}
实战:常见动画效果
按钮悬停效果
.btn {
background: #667eea;
color: white;
padding: 12px 24px;
border-radius: 8px;
transition:
background-color 150ms ease,
transform 100ms ease,
box-shadow 150ms ease;
}
.btn:hover {
background: #764ba2;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn:active {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
}
卡片展开动画
.card {
max-height: 80px;
overflow: hidden;
transition: max-height 300ms ease-out;
}
.card.expanded {
max-height: 500px; /* 足够大的值容纳内容 */
}
.card-content {
padding: 16px;
opacity: 0;
transform: translateY(-10px);
transition:
opacity 200ms ease-out 50ms,
transform 200ms ease-out 50ms;
}
.card.expanded .card-content {
opacity: 1;
transform: translateY(0);
}
模态框动画
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 200ms ease-out;
}
.modal-overlay.show {
opacity: 1;
}
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.9);
background: white;
border-radius: 16px;
padding: 24px;
opacity: 0;
transition:
opacity 200ms ease-out,
transform 300ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.modal.show {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
列表项交错动画
.list-item {
opacity: 0;
transform: translateX(-20px);
animation: slide-in 300ms ease-out forwards;
}
.list-item:nth-child(1) { animation-delay: 0ms; }
.list-item:nth-child(2) { animation-delay: 50ms; }
.list-item:nth-child(3) { animation-delay: 100ms; }
.list-item:nth-child(4) { animation-delay: 150ms; }
.list-item:nth-child(5) { animation-delay: 200ms; }
@keyframes slide-in {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
加载动画
.spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* 脉冲效果 */
.pulse {
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(0.95); }
}
动画与用户体验
动画的目的
好的动画服务于以下目的:
- 提供反馈:告诉用户操作已被接收
- 引导注意力:将用户引导到重要的变化
- 建立空间认知:帮助用户理解界面元素的关系
- 减少认知负担:让变化过程可见,而不是突然发生
动画不应做的事
- 不要太长:等待让人烦躁
- 不要太花哨:动画是手段,不是目的
- 不要中断用户:用户正在操作时不要播放干扰性动画
- 不要误导:动画应该反映真实物理世界的行为
减少动画的原则
某些情况下应该减少或关闭动画:
/* 尊重用户的减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}