抖动动画必须用 transform: translatex() 而非 margin 或 left,以避免重排卡顿;需合并已有 transform、写满五帧关键帧、加 animation-fill-mode: forwards,并精准作用于出错元素,同步控制类名、html 属性与 css 选择器。

抖动动画必须用 transform: translateX(),别碰 margin 或 left
移动端和桌面端都一样:用 margin 或 left 触发抖动,会强制浏览器重排(reflow),连续抖动时卡顿明显,尤其在 iOS Safari 和低端 Android 上容易掉帧。只有 transform: translateX() 走 GPU 合成,无布局计算,响应快、不卡。
如果元素已有其他 transform(比如 scale(1)),不能单独写 translateX(4px)——会被覆盖。必须合并:transform: scale(1) translateX(4px)。
常见错误:
-
animation-name拼错,如写成shak或大小写不一致(CSS 对动画名大小写敏感) - 漏写
animation-duration,默认是0s,动画瞬间完成,根本看不到抖 - 父容器设了
overflow: hidden,抖动位移超出边界被裁剪,可临时加outline: 1px solid red辅助排查
@keyframes shake 必须写满五个关键帧,首尾都是 translateX(0)
只写 0% 和 100% 两个关键帧,浏览器会线性插值,结果是“滑”过去,不是“弹”。抖动失去警示感,用户感知弱。
标准写法必须覆盖五步节奏,幅度控制在 ±4px~±6px,总时长 0.4s:
@keyframes shake {
0% { transform: translateX(0); }
25% { transform: translateX(4px); }
50% { transform: translateX(-4px); }
75% { transform: translateX(4px); }
100% { transform: translateX(0); }
}
务必加 animation-fill-mode: forwards,否则动画播完瞬间回弹,视觉上像“抽搐”,而非稳定提示。
触发抖动要精准到出错字段,且每次都要清理旧类
给整个 <form></form> 加 shake 类,既掩盖具体问题字段,又容易因类名未变导致浏览器跳过重绘——连续失败时可能完全不抖。
目标必须是出错的 <input>、<select></select>,或其直接包裹容器(如 <div class="form-group">)。
<p>正确触发逻辑:</p>
<ul>
<li>校验失败后,先执行 <code>element.classList.remove('shake')
void element.offsetWidth 强制重排(确保浏览器识别状态变化)element.classList.add('shake')
animationend 清理类,但 iOS Safari 可能漏触发,建议加 500ms 超时兜底Bootstrap 的 is-invalid 类和抖动必须同步手动控制
Bootstrap 不会自动加 is-invalid 类,也不会因为你调了 setCustomValidity('msg') 就触发抖动。两者完全解耦,必须你手动桥接。
关键动作缺一不可:
-
<form></form>标签带novalidate属性(否则原生弹窗抢在抖动前出现) - 校验失败时,同步执行:
input.setCustomValidity('错误信息')+input.classList.add('is-invalid')+input.classList.add('shake') - 校验通过时,必须清空:
input.setCustomValidity('')+input.classList.remove('is-invalid', 'is-valid', 'shake') - 如果用了
form-floating,<div class="invalid-feedback"> 仍需紧邻 <code><input>,DOM 结构不能断开最容易被忽略的点不在动画本身,而在「状态同步」:JS 控制类、HTML 属性、CSS 选择器三者必须同时对齐,差一个,抖就没了,红框也不显,提示也不出。











