animation-fill-mode: forwards 必须与 animation 同时声明且关键帧需显式定义100%状态,否则无法锁住结束帧;移动端需注意 display、transform 前缀、JS 动态设置及 requestAnimationFrame 读取时机。

移动端 CSS 动画播完立刻“弹回去”,不是动画写错了,而是 animation-fill-mode 默认是 none,浏览器压根不保留结束帧——加 forwards 就能锁住,但光加这一行常不管用,得看它有没有真正生效。
animation-fill-mode: forwards 必须和 animation 同时存在才起作用
很多人把 animation-fill-mode: forwards 单独写在另一个 CSS 规则里,或者写在被 JS 覆盖的样式块中,结果无效。它不是独立生效的“开关”,而是依附于正在运行的动画。
- 推荐直接用简写:
animation: slideIn 0.3s ease-out forwards;,避免遗漏或覆盖 - 如果必须分开写,确保
animation-fill-mode和animation-name/animation-duration在同一选择器、同一权重下声明 - JS 动态设置动画时,别只改
element.style.animation,记得同步设element.style.animationFillMode = 'forwards',否则内联样式会丢掉 CSS 文件里的 fill-mode
关键帧里没写全 100% 状态,forwards 也救不了
移动端渲染更敏感,@keyframes 中漏掉最终帧定义,会导致浏览器用初始值“补全”,forwards 锁住的就是这个错误值。
-
transform尤其危险:比如只写了0% { transform: translateX(0); }和50% { transform: translateX(100px); },但没写100%,浏览器不会自动延续 50% 的值,而是回退到未声明状态(等价于transform: none) - opacity、visibility、height 这类属性同理,必须显式写出
100% { opacity: 1; }才能被forwards捕获 - 避免用
to代替100%——部分老旧 Android WebView 对to解析不稳定,显式写100%兼容性更稳
移动端特有的覆盖干扰:-webkit-transform 和 display
安卓 WebView 和 iOS Safari 对复合属性和显示逻辑更严格,几个常见干扰点:
-
display: none优先级高于opacity: 0或transform位移,即使forwards锁住了opacity: 1,若父元素或自身有display: none,元素依然不可见 - 某些安卓机型对
-webkit-transform和标准transform并存时行为不一致,建议统一用带前缀的写法,或用 Autoprefixer 保证输出完整 - 页面缩放(
viewport中user-scalable=yes)可能触发重排,意外重置动画层,测试时关闭缩放更易复现问题
JS 触发后读不到最终样式?requestAnimationFrame 是唯一可靠时机
在 animationend 回调里直接调用 getComputedStyle(el).transform,大概率拿到的是初始值或中间值——因为渲染尚未完成,forwards 只影响渲染层,不立即更新计算样式。
- 正确做法:在
animationend里调用requestAnimationFrame(() => { /* 读取 computedStyle */ }) - 避免在动画进行中反复增删 class,尤其在快速点击场景下,iOS Safari 可能丢帧导致
forwards不触发 - 调试时打开 Safari/Chrome 的“Layers”面板,确认动画结束后是否真生成了持久渲染层;没生成,说明
forwards被覆盖或未生效
最易被忽略的一点:forwards 锁住的是“最后一帧声明的值”,不是“你期望的值”。它不会合并、推断或补全任何属性,写多少,它就留多少。移动端没有容错余地,每一行 @keyframes 都得亲手写实。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











