CSS动画默认在播放结束后会释放对元素样式的控制,元素重新回到没有动画时定义的样式。很多操作元素淡入、位移的交互场景,需要动画完成之后停在最后一帧,而不是跳回初始状态。这个需求可以用一个核心属性解决:animation-fill-mode。

不过,真正写项目时只是加上forwards并不总能一次生效,尤其是涉及transform、opacity等多个属性,或者多个动画同时作用时,还要理解基础样式、动画简写顺序和其他替代方案的差异。下面从原因、用法、替代方案和注意事项几个角度展开。
一、动画结束后为什么会跳回原样
CSS动画由@keyframes定义一组关键帧,浏览器在animation-duration规定的时间内依次计算每一帧的样式。动画运行期间,关键帧中的属性会覆盖元素的基础样式,但这种覆盖只发生在动画的持续时间内。一旦动画结束,浏览器会停止将关键帧样式应用于元素,元素的呈现就回到animation属性不存在时的状态。
举例来说,如果元素本身没有设置opacity,而动画从opacity:0过渡到opacity:1,动画结束后元素不会保留opacity:1,而是回到默认的opacity:1。这听起来刚好符合预期,但假如元素基础样式是opacity:0,动画只在中间阶段把它变为可见,结束后又会突然隐藏。很多淡入动画失败,就是因为结束状态没有同时落实到元素本身。
/* 元素基础样式是透明,动画结束后会跳回透明 */
.box {
opacity: 0;
animation: fadeIn 1s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
这个例子中,动画播放期间元素逐渐显示,但结束后由于基础样式仍然生效,元素会立刻消失。要解决这个问题,就需要让最后一帧的样式继续作用于元素。
二、使用 animation-fill-mode: forwards 实现保持
animation-fill-mode控制动画执行前后是否将动画样式应用到元素上。它有四个取值:none、forwards、backwards、both。其中forwards表示动画结束后保留最后一个关键帧的样式,这正是让元素停留在最终状态的关键。
上面的例子只需要改一行:
.box {
opacity: 0;
animation: fadeIn 1s ease forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
添加forwards后,动画结束时to帧中的opacity:1会继续保留。需要注意,forwards只作用于动画结束之后的状态,不会改变动画开始前的延迟阶段。如果希望动画在延迟期间就应用第一帧样式,可以使用backwards;如果同时需要延迟阶段应用第一帧、结束后保留最后一帧,则使用both。
简写属性中的位置也值得留意。标准写法是animation: name duration timing-function delay iteration-count direction fill-mode play-state。因此forwards通常放在时间参数之后,例如animation: fadeIn 1s ease 0s 1 normal forwards。如果顺序写错,浏览器可能把forwards当作动画名称或其他参数,导致无效。
三、其他几种保持最终状态的方法
如果不想使用animation-fill-mode,也可以从基础样式入手。直接把动画结束时的目标值写在元素的普通样式中,这样即使动画结束,元素也不会产生视觉跳动。前面淡入的例子中,把.box的基础opacity改为1即可。这种方式适合结束状态与元素常规状态一致的场景,维护起来更直观,缺点是无法覆盖那些只在动画结束后才应该存在、平时并不需要的样式。
.box {
opacity: 1;
animation: fadeIn 1s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
对于只有开始和结束两个状态的简单交互,使用transition配合类名切换也可以避免跳回问题。transition本身只描述属性变化过程,属性最终值始终来自元素当前样式,因此只要最终样式已经写在元素上,自然不会有“动画结束丢失”的困扰。
.box {
opacity: 0;
transition: opacity 1s ease;
}
.box.active {
opacity: 1;
}
还可以借助JavaScript监听animationend事件,在动画结束后添加一个持久类名。此方案更灵活,可以在动画结束后继续执行其他逻辑,例如更新状态、移除动画类、避免重复触发等。
const box = document.querySelector('.box');
box.addEventListener('animationend', function () {
box.classList.add('animation-done');
});
.box {
opacity: 0;
animation: fadeIn 1s ease;
}
.box.animation-done {
opacity: 1;
animation: none;
}
这段代码在动画结束后把opacity固定为1,同时移除动画,避免后续状态被再次触发。缺点是增加了一定脚本依赖,但可以处理更复杂的条件。
四、实际使用中的注意点
当动画属性包含transform时,forwards保留的最后一帧会覆盖元素原来的transform。如果元素同时需要承担其他位移或旋转,可能会出现位置偏移。例如基础样式设置了transform: translateX(20px),而动画的最后一帧是transform: translateX(0),动画结束后元素会停在translateX(0),不再是20px。因此要确保动画结束帧与元素后续状态的transform一致,或者把目标值完整地写在结束帧和基础样式中。
多个动画同时作用于同一个元素时,后声明的动画会覆盖前一个动画对同一属性的控制。要想让多个动画各自保持结束状态,可以在每个动画的简写中分别加上forwards,并且不要在两个动画中同时操作相同的CSS属性。如果确实需要叠加transform,建议把位移、缩放、旋转拆到不同层级的元素上,从结构上避免冲突。
另外,display属性无法在动画关键帧中平滑过渡,也不会通过forwards可靠地保持显示或隐藏状态。如果动画结束需要元素保持可见,最好使用opacity或visibility控制隐藏效果,而不是直接切换display:none。对于需要从页面中移除元素的场景,可以在动画结束后通过JavaScript再设置display,避免动画和布局相互干扰。
最后,animation-fill-mode: forwards兼容现代浏览器和较新的移动端环境。对于非常老旧的浏览器,可以结合基础样式降级,让元素即使没有保持动画帧也能正常显示。实际项目中优先采用基础样式与forwards配合的方式,既简单又稳定。
CSS动画animation-fill-modeforwards修改时间:2026-08-25 08:51:39