animationend 不触发的根本原因是 CSS 动画未真正执行,常见于初始与终态相同、动画时长为0、元素未挂载或 display: none、animation-fill-mode 误用等;需用 getComputedStyle 验证真实状态,避免依赖 event.animationName,兼容性差时应加 setTimeout 回退。

animationend 事件监听不到?检查 CSS 动画是否真正触发
很多情况下 animationend 根本不触发,不是 JS 写错了,而是 CSS 动画压根没跑起来。常见原因有:
- 元素初始状态和动画终点状态完全一样(比如 opacity: 1 → opacity: 1),浏览器直接跳过动画
- 动画时长设为 0s 或被 animation-duration: 0 覆盖
- 元素未插入 DOM 或处于 display: none 状态,动画不会启动
- 使用了 animation-fill-mode: none(默认值),但更关键的是:动画必须「开始播放」才会触发事件,哪怕只播一帧
移除动画类后 DOM 还残留样式?用 getComputedStyle 判断真实状态
动画结束后手动移除 class 很常见,但移除后元素可能还保留着最后一帧的样式(比如 transform: translateX(100px))。这是因为 animation-fill-mode 默认是 none,但如果你用了 forwards,动画结束帧会保留——而这时 JS 移除 class 后,浏览器会回退到 class 不存在时的原始样式,造成“闪一下”或位置突变。
- 若需彻底还原,确保动画 class 中不设置影响最终布局的属性(如避免在动画里改 height、display)
- 若必须保留终态,别急着删 class,先用 getComputedStyle(el).transform 拿到终值,再用 JS 显式赋给 style.transform,最后删 class
- 更稳妥的做法:动画只负责“过渡”,终态由另一个 class 控制,动画结束时切换 class,而非删除
多个动画同时存在时 event.animationName 判定不准
一个元素上定义了多个 @keyframes,又都通过 class 启用,animationend 触发时,event.animationName 只返回最先结束的那个名字,无法区分谁真结束了、谁被覆盖中断了。
- 不要依赖 event.animationName 做关键逻辑分支,尤其当有 animation-name: slide, fade 这种多值写法时
- 更可靠的方式:给每个动画绑定独立 class,用 el.classList.contains('animate-slide') 配合 setTimeout 回退兜底(比如 300ms 后强制清理)
- 如果必须精确匹配,改用 el.getAnimations()(现代浏览器支持),遍历 .finished 状态,但注意它返回的是 Animation 实例,不是 CSS 类名
兼容性与性能:animationend 不是万能钩子
animationend 在 Safari 旧版本、Android Webview 里行为不稳定,比如重复触发、漏触发,或者在硬件加速关闭时失效。它也不是同步事件——动画实际结束和事件派发之间有微小延迟。
- 关键交互(如按钮禁用后恢复)不要只靠 animationend 解锁,加个 setTimeout(fn, animationDuration + 50) 作为 fallback
- 避免在 animationend 回调里做重排(如读取 offsetHeight 后立刻改 style),容易引发强制同步布局
- 如果只是想“动画完就隐藏”,直接用 visibility: hidden + animation-fill-mode: forwards,比 JS 控制更轻量
最常被忽略的一点:CSS 动画是否启用,取决于「是否产生样式变化」,而不是「有没有 class」。哪怕 class 已加上,只要计算出的样式没变,动画就不会执行,animationend 自然也不会来。










