真正可用的shake动画必须首尾归零、中间非对称、位移值小:0%和100%设为translateX(0),25%→-4px、50%→3px、75%→-2px,时长0.3s–0.5s,不加animation-fill-mode。

直接用 @keyframes + animation 是最可控、最轻量的方式,其他方案(如第三方库、JS 手动重绘)要么增加体积,要么难维护或性能差。
怎么写一个真正可用的 shake @keyframes?
关键不是“抖得多”,而是“停得准、看起来像被撞了一下”。必须满足三点:首尾归零、中间非对称、位移值小。
- 0% 和 100% 都设为
transform: translateX(0),否则元素会偏移原位置,影响布局 - 中间帧别写成镜像(比如 25% 和 75% 都是
translateX(-4px)),推荐用-4px → 3px → -2px这类不对称组合 - 位移值用整数像素(
2px/4px/6px),避免 subpixel 渲染模糊;别超±8px,否则像弹跳不是震动 - 总时长控制在
0.3s–0.5s,少于0.2s感知不到,超过0.6s在 iOS 上易掉帧
示例:
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-4px); }
50% { transform: translateX(3px); }
75% { transform: translateX(-2px); }
}
为什么 animation-fill-mode: forwards 有时要加,有时不能加?
这取决于你是否需要动画结束后“保持最后一帧状态”。震动场景下,100% 已经是 translateX(0),加 forwards 没意义,反而可能干扰后续 JS 动态修改 transform。
立即学习“前端免费学习笔记(深入)”;
- 加
forwards的典型场景:滑入动画(opacity: 0 → 1)、下拉菜单展开(height: 0 → auto)——这些动画结束态 ≠ 初始态 - 震动动画不用加:因为
0%和100%相同,加了等于没加;且若之后 JS 要再触发一次震动,残留的forwards状态可能让浏览器跳过重绘 - 真要保险:就别写
animation-fill-mode,用默认none
JS 触发震动时最容易踩的三个坑
震动不是装饰,是反馈信号。JS 控制不到位,用户会以为页面卡了、或者误操作了。
- 没强制重排就重复 addClass:浏览器可能合并动画帧,导致只抖一次。修复方式是加
void el.offsetWidth或用getComputedStyle(el).transform强制触发 layout - 没清理旧 class:连续校验失败多次,
classList.add('shake')叠加多次,动画堆在一起播放。务必先remove再add - 没监听
animationend:靠 timeout 移除 class 不可靠(设备卡顿时动画延迟,class 提前消失)。正确写法:el.addEventListener('animationend', () => el.classList.remove('shake'), { once: true }) - 移动端要注意:iOS Safari 对
translateX高频动画有节流,如果父容器有overflow: hidden或will-change: transform,震动可能直接不播
震动幅度和时长没有标准答案,但真实项目里最常被忽略的是「非对称关键帧」和「强制重排」——前者决定是不是真抖,后者决定抖不抖得出来。


















