用 animation + animation-fill-mode: forwards 可实现首次加载动画只执行一次并保持终态,关键在于设置 animation-iteration-count: 1 和 animation-fill-mode: forwards,并确保类名在元素挂载 DOM 后添加,避免依赖 transition 或重复加类触发。

用 animation + animation-fill-mode: forwards 控制首次加载动画
CSS 动画默认会在元素插入 DOM 后立即触发,但如果你用类名(比如 .animate-in)控制,又希望它只在首次添加时执行、后续增删该类不再重复播放,关键不是靠 JS 防重,而是靠 CSS 自身的「播放一次 + 保持终态」机制。
核心是两个属性配合:animation-iteration-count: 1(确保只播一次)和 animation-fill-mode: forwards(让动画结束后样式停留在最后一帧,避免闪回或状态错乱)。
-
animation-fill-mode: none(默认)→ 动画结束立刻恢复初始样式,肉眼可能看到“回退” -
animation-fill-mode: forwards→ 动画停在@keyframes最后一帧的样式,视觉上更自然,也方便后续基于终态做交互 - 别漏写
animation-duration和animation-timing-function,否则动画不生效
类名添加时机必须在元素已挂载 DOM 后
如果用 JS 动态加类(如 el.classList.add('animate-in')),但元素刚创建完还没插入文档(比如 document.createElement('div') 之后立刻操作),动画不会触发。浏览器需要真实渲染上下文才能启动 CSS 动画。
常见错误写法:
立即学习“前端免费学习笔记(深入)”;
const el = document.createElement('div');
el.classList.add('animate-in'); // ❌ 此时 el 还没 append 到 body,动画不触发
document.body.appendChild(el);正确做法(任选其一):
- 先挂载,再加类:
document.body.appendChild(el); el.classList.add('animate-in'); - 或用
requestAnimationFrame延迟到下一帧:document.body.appendChild(el); requestAnimationFrame(() => { el.classList.add('animate-in'); }); - Vue/React 等框架中,确保在
mounted或useEffect的 DOM 已就绪回调里操作
避免重复触发:不要用 transition 模拟动画起始态
有人会想“我先设个透明态,再加类改 opacity”,结果发现每次加类都触发——这是因为 transition 是持续监听属性变化的,只要值变了就动。而你真正要的是「仅首次」,就得切断后续响应链。
安全做法是:动画完全由 animation 承担,初始态通过普通 CSS 写死,不依赖 transition 衔接。
错误示例(易重复触发):
.box {
opacity: 0;
transition: opacity 0.3s;
}
.box.show {
opacity: 1;
}正确示例(真正只播一次):
.box {
opacity: 0; /* 初始态明确 */
}
.box.animate-in {
animation: fadeIn 0.3s ease-out 1 forwards;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}JS 主动控制时,注意 classList 切换的原子性
如果逻辑里需要“移除再加回类名来重播动画”,那确实会再次触发——这恰恰违背了“只首次”的需求。所以得加一层守卫:
- 用
getComputedStyle(el).animationName检查是否已在播放(但兼容性一般) - 更可靠的是用自定义 data 属性标记:
if (!el.dataset.animated) { el.classList.add('animate-in'); el.dataset.animated = 'true'; } - 或者直接移除类后再强制重排(不推荐,性能差):
el.classList.remove('animate-in'); void el.offsetWidth; // 强制 reflow el.classList.add('animate-in');
真正只首次执行,本质是「状态 + 样式分离」:CSS 负责表现,JS 负责守门。动画本身没有“记忆”,靠外部状态来约束。










