优先用 transition 实现有明确起点和终点的简单交互(如悬停变色、点击缩放),而循环、多阶段或自动播放动画必须用 @keyframes + animation;二者底层机制不同,可分工协作但不可替代。

直接结论:鼠标悬停变色、按钮点击缩放、菜单展开收起这类有明确起点和终点的交互,优先用 transition;需要循环、暂停、多阶段或自动播放(比如 loading spinner、打字效果),必须用 @keyframes + animation。
什么时候 transition 就够用了
它只管“从 A 到 B 怎么走”,不关心中间怎么停、要不要重播、有没有第 3 帧。只要你的交互满足这几个条件,transition 更轻、更稳、更容易维护:
- 触发源明确:靠
:hover、:focus、JS 切换class或直接改style - 起止状态都写了具体值:比如默认状态有
transform: scale(1),hover 时写transform: scale(1.1)—— 缺一不可,否则过渡不生效 - 只动一两次:进出各一次就够了,不需要反复循环或中途暂停
- 只改可硬件加速的属性:
transform和opacity最安全;避免用width、height、left、top,否则可能触发重排,卡顿明显
示例:一个按钮悬停放大并变色
button {
background: #007bff;
transform: scale(1);
transition: background 0.3s ease, transform 0.2s ease;
}
button:hover {
background: #0056b3;
transform: scale(1.05);
}
为什么不能硬套 transition 做多阶段动画
常见翻车现场是给整个元素加 transition: all 0.3s,然后靠 JS 快速连续改多个属性(比如先平移、再旋转、再透明)——浏览器会把所有变化“合并成一次过渡”,结果就是动画跳帧、队列堆积、鼠标进出频繁时直接卡死。
立即学习“前端免费学习笔记(深入)”;
-
transition没有时间轴概念,无法定义 25%、50% 这样的中间状态 - 它只响应“属性值变了”这个事实,不区分变化是来自 CSS 还是 JS,也不管这次变化是不是你想要的“那一帧”
- 如果 JS 在动画进行中又改了同一个属性(比如正在缩放时又调用
scale(1.2)),当前过渡会被中断重置,用户看到的就是“抽搐” - 想控制循环次数、反向播放、暂停?
transition完全没这些 API
animation 不是万能解药,用错一样崩
它强在可控,但代价是复杂度上升,且容易忽略关键细节:
- 必须显式定义
@keyframes,哪怕只写0%和100%—— 漏掉就啥也不动 - 动画结束后默认回退到初始样式,得加
animation-fill-mode: forwards才能“定格”在最后一帧 - 用
:hover触发animation时,鼠标进出反复触发,动画就会不断重头开始;要暂停/继续,得手动控制animation-play-state - 老 Android WebView 对
cubic-bezier()解析不准,复杂缓动曲线可能失效,建议用ease或linear保底
示例:hover 启动呼吸动画,且进出都平滑
.pulse-btn {
animation: pulse 1s ease-in-out;
animation-fill-mode: forwards;
}
.pulse-btn:hover {
animation-play-state: running;
}
@keyframes pulse {
0% { opacity: 0.8; }
100% { opacity: 1; }
}
混用才是真实项目里的常态
现代 UI 很少非此即彼。比如一个带图标的折叠菜单:
- 菜单高度变化用
transition(平滑展开/收起) - 图标旋转用
animation(加弹性反馈,比如旋转后轻微回弹) - 伪元素发光效果单独跑
animation,不干扰主元素的transition
注意:同一属性(比如都动 transform)上,animation 会覆盖 transition,不是叠加。真要协同,得错开属性(如 transition 控制 scale,animation 控制 rotate)或分层(主元素 + ::before)。
最常被忽略的点是:transition 依赖属性“计算值”的变化,而 animation 是独立时间轴驱动 —— 这个底层差异决定了它们根本没法互相替代,但可以分工协作。


















