如果你觉得 CSS 动画一眼就能看出来“假”,那问题绝大多数不在位移距离,也不在动画时长,而在 transition-timing-function 和 animation-timing-function 这两个被很多人忽略的属性上。CSS 动画的真实感不靠引入物理引擎,靠的是把速度随时间变化的曲线调对,让元素看起来“有重量、有惯性、有阻力”。
这篇内容会围绕缓动函数展开:从内置关键字到 cubic-bezier() 的调参逻辑,再到 steps() 这种离散节奏的用法,最后给你一套可以直接抄走的实际项目模板,并附上我在调试过程中踩过的一些坑。适合写后台组件、官网动画、落地页动效的前端工程师和 UI 动效设计师阅读,也适合刚接触前端动画、想搞清楚“为什么别人写的动画那么顺滑”的同学。
1. 动画的真实感不来自“像”,而来自“节奏对”
1.1 为什么默认的 ease 和 linear 总让人觉得生硬
早年间我做弹窗的时候,习惯性写了 transition: all .3s ease,当时觉得 ease 已经是所有缓动函数里最常用的了,应该没毛病。后来设计师看了一眼说“这个弹层像假的”,我第一反应是阴影不够深,结果加了一堆模糊度还是不太对,最后逐帧录制比对才发现问题出在节奏上:元素从头到尾的“速度变化”太呆板,入场感觉像是被程序强制从 A 点搬到 B 点,缺少现实世界里物体该有的启动和刹停过程。
这里先说一个基础认知:所谓动画帧,本质是让元素属性值在一个时间段内从旧值变成新值。如果时间函数是线性的,那么每一帧的位移量完全一致。但真实世界的物体运动几乎不存在这种匀速状态,汽车起步有加速过程,飞机降落后还要滑行减速,连橡皮从桌上滚落都会经历一个先慢后快、触地后又反弹的过程。如果动画里的物体从头到尾一个速度跑完,大脑就会第一时间判断“这个物件没有质量,不符合常理”。
CSS 默认的 ease 确实带一点加速减速的意思,但它更像是一个“通用中庸值”,不针对任何具体动作。你想让弹窗有“被弹簧推出来”的感觉,或者让卡片有“被手指按下去再弹回来”的反馈,需要的是能表达过冲、回弹、延时响应的自定义曲线。而这正是 cubic-bezier() 的强项。
1.2 时间曲线的本质:重新分配每一毫秒
说直白一点,缓动函数改变的并不是动画的距离,而是在总时长不变的情况下,重新分配“每一帧完成多少进度”。用一个生活例子来理解:同样一条 100 米跑道,普通走路是匀速前进,适合形容线性动画;骑自行车起步时用力蹬,速度逐渐拉起来,到了终点前再捏刹车,这段速度分布就是标准的“先加速后减速”曲线;而如果你从高处往下跳,落地前速度越来越快,落地后还要缓冲一下,那就是另一套完全不同的节奏。
放进 CSS 里,所谓 ease-in 就是“慢启动、快结束”,适合表达自由落体这种不断加速的动作;ease-out 是“快启动、慢结束”,适合表达物体被释放、逐渐停稳的滑动效果;ease-in-out 则两端都比较温和,适合不希望给人忽然启动感的 UI 过渡。但真正的细节藏在自由度更高的 cubic-bezier() 和 steps() 里,前者能调出连续光滑的物理质感,后者能表达齿轮跳动、打字机逐字显示这类离散节奏。
还有一个很多人不知道的点:CSS 里内置的 ease、ease-in、ease-out、ease-in-out,本质上就是几个预设的 cubic-bezier 曲线。你完全可以不背这些关键字,直接记住它们背后的数值,这样写自定义动效时会更清楚自己在调什么。下表是这些默认曲线的坐标值,方便对照理解:
| 关键字 | 等效 cubic-bezier | 曲线特点 |
|---|---|---|
ease |
cubic-bezier(0.25, 0.1, 0.25, 1.0) |
中段加速快,两端收得慢,通用 |
ease-in |
cubic-bezier(0.42, 0, 1.0, 1.0) |
慢启动,后面越来越快,像坠落 |
ease-out |
cubic-bezier(0, 0, 0.58, 1.0) |
启动快,越到后面越慢,像刹车停稳 |
ease-in-out |
cubic-bezier(0.42, 0, 0.58, 1.0) |
两端都缓慢,中间加速,最常用 |
linear |
cubic-bezier(0, 0, 1, 1) |
匀速直线运动 |
1.3 这篇内容能帮你在哪些场景落地
如果你只是写一个鼠标 hover 改变颜色,那随便用个 ease 也就够了。但一旦你的工作里出现以下这些场景,缓动函数就不是可有可无的修饰品了:
- 组件库里的弹窗、抽屉、菜单展开收起,用户每次点击都要看到,假一点都会被放大感知;
- 官网 Banner 里的滚动入场、图片位移、文字逐行显现,节奏直接影响品牌质感;
- 数据可视化大屏里数字滚动、表格行展开、进度条增长,曲线调对了,“高级感”立刻上一个台阶;
- 游戏化 H5 或营销页面里的按钮反馈、卡片翻转、礼物弹跳,这些动作本身就来自现实世界的物理感,要求更高。
接下来的内容会从参数拆解和实操两个方向展开。你可以理解原理后直接跳到对应的项目模板抄作业,也可以从头到尾顺一遍,下次拿到动效需求时不再只会问“时长用 300ms 还是 500ms”,而是能说出“这里应该用一个带 1.2 倍过冲的 easeOutBack”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. transition 与 animation 里的缓动函数到底在管哪一段
2.1 CSS 里两个 timing-function 的生效位置不一样
很多人写了 transition: left 1s cubic-bezier(...) 只是机械地认为“过程用这个曲线”,其实 transition 和 animation 的 timing function 作用粒度完全不同。
transition-timing-function 作用于“从一个状态到另一个状态”的单次补间过程。鼠标悬停时元素变大是一次 transition,移出时从大到小又是一次独立的 transition。两次过程可以分别设置不同的曲线,这在做按钮按下回弹效果时特别好用。
animation-timing-function 则作用于“关键帧与关键帧之间”。你可以把 @keyframes 里的 0%、50%、100% 看成一个个站点,animation-timing-function 控制的是“从上一个关键帧到下一个关键帧之间”的变速情况。因为一个动画里可能有多段路程,你完全可以让 0% 到 50% 用
