animate.css 4.x JavaScript 集成指南:动态添加动画、监听 animationend 与 Promise 封装实战
animate.css 是一款跨浏览器的纯 CSS 动画库,除了直接在 HTML 中写 class 之外,配合 JavaScript 还能实现动态触发动画、在动画结束后执行后续逻辑、以及运行时实时调节动画时长。本文基于仓库文档 Usage with Javascript 展开,并结合 source/_base.css、source/_vars.css 与 postcss.config.js 的源码实现,帮你理解每一行 JS 背后 CSS 层的机制,从而写出可复用、可等待、可变速的动画控制代码。
前置:animate.css 的动画类命名机制
在看 JS 用法之前,先明确一个前提:动画类由“基础类 + 动画名类”两部分组成,且都带 animate__ 前缀。源码 source/animate.css 只是入口,它通过 @import 聚合各分类目录(如 bouncing_exits/bounceOut.css、fading_entrances/fadeIn.css 等)下的动画定义;而 animate__ 前缀并不是手写的,而是构建时由 postcss.config.js 中的 postcss-prefixer 插件统一加上的,前缀值来自 package.json 里的 animateConfig.prefix 字段(值为 animate__)。因此 JS 中拼接的类名必须与这个前缀保持一致。
注意一个细节:postcss.config.js 中 prefix 的取值为 ctx.env === 'compat' ? '' : animateConfig.prefix——即 animate.compat.css 兼容构建不带前缀,只有 animate.css / animate.min.css 才使用 animate__ 前缀。如果你使用 compat 版本,下面的前缀逻辑需要相应调整。
动态添加动画类
最简单的用法是通过 classList 为元素同时加上基础类和动画类。以一个 bounceOutLeft 出屏动画为例(该类对应源码文件 source/bouncing_exits/bounceOutLeft.css):
const element = document.querySelector('.my-element');
element.classList.add('animate__animated', 'animate__bounceOutLeft');
为什么必须加 animate__animated 这个基础类?查看编译产物 animate.css(源码为 source/_base.css)可以看到,它承担了两个关键职责:
.animated {
animation-duration: var(--animate-duration);
animation-fill-mode: both;
}
animation-duration从 CSS 变量读取,这是后文“运行时改时长”能生效的根基;animation-fill-mode: both保证动画结束后的最终状态(例如淡出到透明、位移后的位置)会保留在元素上,这正是各类Out动画“退出即消失”效果的来源。
而每个具体动画类只负责声明 animation-name 和时长比例。以 source/bouncing_exits/bounceOut.css 为例:
.bounceOut {
animation-duration: calc(var(--animate-duration) * 0.75);
animation-name: bounceOut;
}
也就是说,每个动画的实际时长都是 --animate-duration 的一个倍数(bounceOut 为 0.75 倍)。这种“统一变量 + calc() 比例”的设计,让全局或局部改变一个变量值,就能成比例地缩放所有动画时长。
监听 animationend 检测动画结束
CSS 动画本身不会通知 JS “我已经播完了”。animate.css 依赖标准的 animationend DOM 事件解决这个问题:
const element = document.querySelector('.my-element');
element.classList.add('animate__animated', 'animate__bounceOutLeft');
element.addEventListener('animationend', () => {
// do something
});
当 animation-name 对应的关键帧执行完毕,浏览器会在该元素上触发 animationend,此时就可以执行移除元素、发送请求等后续操作。
使用这个事件时有两个值得注意的点:
- 元素必须真正处于动画状态。若元素没有同时具备
animate__animated和动画类,animationend不会触发;同理,若动画类被移除(见下文封装),要重新触发事件需重新classList.add。 - 减少动画偏好会让事件“瞬间”触发。source/_base.css 在打印媒体和
prefers-reduced-motion: reduce场景下把animation-duration强制为1ms,因此依赖animationend的后续逻辑会几乎立即执行——这对可访问性是正确的行为,但编写异步流程时要有这个预期。
通过 CSS 变量在运行时调整时长
animate.css 4.x 起使用 CSS 自定义属性统一定义动画的时长、延迟与重复次数,默认值定义在 source/_vars.css:
:root {
--animate-duration: 1s;
--animate-delay: 1s;
--animate-repeat: 1;
}
这意味着 JS 一行代码就能改变某个元素(甚至全站)的动画速度,无需重写任何 keyframes:
const element = document.querySelector('.my-element');
element.style.setProperty('--animate-duration', '0.5s');
由于变量可以设置在不同层级,作用域也不同:
// 全局:所有动画耗时减半
document.documentElement.style.setProperty('--animate-duration', '.5s');
// 全局:所有动画耗时翻倍
document.documentElement.style.setProperty('--animate-duration', '2s');
- 设置在
document.documentElement上即等效于覆盖:root的默认值,影响页面内所有动画; - 直接设置到具体元素(
element.style)则只影响该元素,利用 CSS 变量沿 DOM 树继承的特性实现“局部变速”; - 由于每个动画类都用
calc(var(--animate-duration) * 比例)定义时长(如 bounceOut 的 0.75 倍),改变变量值时各动画会保持相对比例,不会失真。
封装 Promise 化的 animateCSS 辅助函数
手动“加类名 + 监听 animationend”的组合略显繁琐,文档给出了一个可复用的 Promise 封装函数。完整代码(来自 04-javascript.md):
const animateCSS = (element, animation, prefix = 'animate__') =>
// We create a Promise and return it
new Promise((resolve, reject) => {
const animationName = `${prefix}${animation}`;
const node = document.querySelector(element);
node.classList.add(`${prefix}animated`, animationName);
// When the animation ends, we clean the classes and resolve the Promise
function handleAnimationEnd(event) {
event.stopPropagation();
node.classList.remove(`${prefix}animated`, animationName);
resolve('Animation ended');
}
node.addEventListener('animationend', handleAnimationEnd, {once: true});
});
逐段拆解它的实现要点:
- 参数设计:
element接收 CSS 选择器字符串(内部用document.querySelector解析),animation只传动画名(如'bounce'),prefix默认'animate__',与 package.json 中animateConfig.prefix保持一致; - Promise 生命周期:
new Promise立即返回,resolve('Animation ended')在动画结束时调用,因此调用方可以用then/await精确地“等待动画播完”; {once: true}:监听器在首次animationend后自动移除,无需手动removeEventListener,避免了重复执行清理逻辑的隐患;event.stopPropagation():防止该事件继续冒泡到父元素——这在动画元素嵌套(比如一个卡片里还有子元素动画)时很关键,能避免父级的animationend监听器被误触发;- 清理类名:
handleAnimationEnd中同时移除了animate__animated和动画类。这是刻意为之——保留动画类会让元素停留在 keyframes 的终态(因为fill-mode: both),也可能阻塞下次触发;清理后元素恢复初始样式,随时可以再次调用。
调用方式有两种——不关心结束时机时直接调用,需要串行逻辑时链式处理:
animateCSS('.my-element', 'bounce');
// or
animateCSS('.my-element', 'bounce').then((message) => {
// Do something after the animation
});
配合 async/await 甚至可以编排动画序列:
await animateCSS('.step-1', 'fadeInUp');
await animateCSS('.step-1', 'fadeOutUp');
await animateCSS('.step-2', 'fadeInUp');
实战要点与注意事项
- 前缀必须与构建产物匹配。本文所有示例均针对标准构建(
animate.css/animate.min.css)。从 postcss.config.js 可以看到 compat 构建的前缀为空字符串,若你加载的是animate.compat.css,需将prefix改为''。 animationend是同步契约的前提。Promise 封装只在animationend触发时 resolve。若元素被display: none、不在文档流中,或被媒体查询禁用动画,事件可能迟迟不触发,建议根据业务需要增加超时兜底。- 变量作用域决定影响范围。
--animate-duration设置在:root(或documentElement)是全局变速,设置在单个元素上仅局部生效;同理--animate-delay控制animate__delay-*延迟类、--animate-repeat控制animate__repeat-*重复类的基准值,三者都定义在 source/_vars.css,均可用同一套setProperty手段在运行时调整。 - 减少动画偏好的兼容性代价是速度。
prefers-reduced-motion下所有动画被压缩到 1ms(见 source/_base.css),基于animationend的串行流程会快速走完,这是符合无障碍预期的正确表现,但交互状态机(如轮播、弹窗)应确保快速跳帧后状态依然一致。 - 动画名以源码目录为准。可用的
animation参数名即 source/animate.css 中聚合的各分类文件对应的动画(去掉前缀),例如bounce、fadeInUp、zoomOut、flipInX等,目录结构(attention_seekers/、fading_entrances/、zooming_exits/等)就是现成的动画名清单。
小结
animate.css 与 JavaScript 的协作模型非常清晰:JS 负责“何时加类、何时移除类、如何响应事件”,CSS 变量负责“快慢与比例”,而 animationend 事件把两者缝合成异步可编排的流程。掌握了 classList 动态加类、animationend 监听、--animate-duration 变量三件套,以及一个 Promise 封装函数,就足以覆盖绝大多数“动画驱动界面状态”的实战场景;同时结合仓库中 source/_base.css 与 source/_vars.css 的实现,能准确预判动画在减少动画偏好、compat 构建等边界场景下的行为。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00