CSS3 动画核心属性详解:延迟、填充、次数与方向
2026/9/14 15:54:27 网站建设 项目流程

fill-modedirection,CSS3 动画的核心玩法其实就藏在几个容易被忽略的属性里。这篇我先从动画的延迟、执行次数、逆向播放和完成态保持这四个点展开,把每一处细节和用法讲透。

初学 CSS3 动画的时候,很多人最大的困惑是:明明写好了animation,但元素要么不动,要么动画结束之后“啪”地一下跳回原位,要么循环次数怎么调都不对。这些问题的根源,基本都落在animation-delayanimation-fill-modeanimation-iteration-countanimation-direction这四个属性上。

这个系列的第一篇,我就把这几个属性拿出来单独做一次深度拆解。不会只讲语法,而是把浏览器在执行动画时背后那套状态转换逻辑理清楚,配合实际项目里最常见的几个动画场景,看完你就能直接用到自己的页面上。

这篇文章适合已经掌握了@keyframes基本写法的前端开发者。如果你对 animation 的理解还停留在“写个过渡、转个圈”的层面,那这篇内容应该能帮你把动画能力往上抬一个台阶。

1. 动画状态的底层逻辑:先理解时间线,再谈属性

1.1 动画执行背后的三段式时间模型

要真正弄懂上面四个属性,先得建立一条完整的时间线。CSS3 动画从开始到结束,其实经历了三个阶段:等待阶段播放阶段结束阶段

很多人只关注“播放阶段”,也就是@keyframes里定义的动画过程,却忽略了等待阶段和结束阶段的处理,所以才会出现动画开始前元素状态不对、结束后元素状态突然跳变的问题。

  • 等待阶段:动画还没开始播放,此时元素显示的是哪一个状态,由animation-delayanimation-fill-mode共同决定。
  • 播放阶段:元素按照@keyframes按时间轴逐帧渲染,此时animation-timing-functionanimation-duration等属性发挥作用。
  • 结束阶段:最后一个关键帧渲染完成后,元素应该停留在哪个状态,由animation-fill-mode决定。

举个例子,一个元素要从opacity: 0渐变到opacity: 1,如果只写了animation: fadeIn 2s;,在动画未开始前元素是默认状态(不透明),在动画结束后元素又恢复成默认状态(不透明)。而如果你在关键帧里设定的起始状态是透明,就会产生“动画开始那一下元素是透明,播放完突然跳成不透明”的现象,非常刺眼。

这就是为什么真实项目中几乎离不开animation-fill-mode的原因。

1.2 八个子属性的分工与协作关系

animation是一个复合属性,它由八个子属性组成,完整写法长这样:

.element { animation-name: fadeIn; /* 动画名称 */ animation-duration: 2s; /* 动画时长 */ animation-timing-function: ease; /* 缓动函数 */ animation-delay: 0s; /* 延迟时间 */ animation-iteration-count: 1; /* 执行次数 */ animation-direction: normal; /* 播放方向 */ animation-fill-mode: none; /* 完成后状态 */ animation-play-state: running; /* 播放状态 */ }

这八个属性里,nameduration是必须的,其他都可以缺省。但缺省不代表不生效,缺省时使用初始默认值。真正的坑就在这里:很多人在简写里只写了几个值,剩余属性用了默认值,结果和自己预期的动画行为完全不一样。

比如简写animation: fadeIn 2s 1s;,这里第三个时间值会被解析为animation-delay,而fill-mode仍然保持默认的none。所以动画开始前会白等一秒,结束后元素照样跳回默认状态。

建议:在写正式项目时,尽量把需要控制的属性显式声明出来,不要过度依赖简写。简写确实简洁,但一旦涉及延迟和结束状态控制,它带来的隐性问题远比省下的几个字符更费时间。

2. 动画延迟与完成后状态的保持

2.1 animation-delay 的细节:不只是“晚一点开始”

animation-delay很多人用过,但它的两个细节很容易被忽略。

第一个细节是负值延迟animation-delay可以写成负值,比如animation-delay: -1s。它的效果是:动画会立刻从“已经播放了 1 秒”的中间状态开始播放。

这个特性非常实用。举个例子,你要做一个持续的旋转加载图标,动画总时长是 2 秒,但如果页面有多个图标同时出现,你希望它们的旋转相位错开,而不是所有图标完全同步地转圈。此时不需要给每个图标设置不同的延迟值再去等待,直接设置不同的负延迟即可,比如animation-delay: -0.5sanimation-delay: -1s。负延迟让动画立即启动,但起点各不相同,视觉上就能形成错落的节奏感。

第二个细节是延迟期间的元素状态。正延迟期间,元素有多少时间处于“等待”状态,那这段时间元素长什么样,取决于animation-fill-modebackwards值。如果没有开启backwards,等待期间元素保持默认状态,而不是@keyframes的起始状态。

这在做“进场动画”时特别典型。页面加载后,一个卡片要在 800ms 延迟后才从透明逐渐浮现,如果等待期间卡片是清晰的、可见的,然后动画一开始突然变透明,再慢慢浮现,整个观感就会出现闪跳。

2.2 fill-mode 四个值的行为边界

animation-fill-mode官方定义了四个值,分别是noneforwardsbackwardsboth。搞清楚这四个值的适用场景,能解决一大部分动画“跳变”问题。

先看一张对照表帮助理解:

填充模式等待阶段(delay 期间)播放阶段结束后
none默认状态正常播放默认状态
forwards默认状态正常播放保持最后一帧
backwards应用第一帧状态正常播放默认状态
both应用第一帧状态正常播放保持最后一帧
  • none:默认值。动画前后都不做任何状态填充。适用于那些只在播放期间需要动画效果、结束后元素就要回到默认状态的场景,比如按钮点击时的脉冲效果。
  • forwards:动画结束后保留最后一帧的状态。最常见的应用就是“淡入一个弹窗”,动画结束后弹窗要保持完全可见,不能被重置回透明状态。
  • backwards:在delay等待期间,把第一帧状态应用上去。这个值解决的就是 2.1 里提到的“等待期间元素状态不对”的问题。
  • both:等于forwards+backwards,是真实项目中最常用的值。既解决延迟等待期的状态问题,也解决结束后的状态驻留问题。

实际经验是,大部分进场动画直接写animation: xxx 0.6s ease both;,基本不会出大问题。它能保证动画开始前状态正确,结束后状态也正确。

2.3 要做到“完成后保持”,还得搭配forwardsboth

还有一个高频错误是:只写了animation-fill-mode: fowards(注意这里拼写必须是forwards,带 s),但发现结束状态依然不对。

排查思路要分两步:

第一步,确认关键帧的最后一帧确实是你想要的状态。fill-mode: forwards保留的是@keyframes100% 那一帧的状态。如果关键帧里只写了fromto,那它保留的是to的状态。如果你需要的最终状态不在关键帧里,那就得把to的定义改掉。

第二步,检查是否有其他样式规则覆盖了动画状态。animation在层叠优先级上处于较高层级,但如果后续有组件内部的样式使用了!important,且属性恰好是动画改变的属性,那么动画结束后的状态仍然会被覆盖。

我遇到过一种很隐蔽的情况:一个卡片组件在 .card 类里定义了background-color: #fff,动画在结束状态把背景色改成了黄色,但因为组件库的样式表用了!important,导致动画结束后背景色又变回了白色。排查了很久才发现是层叠关系的问题。

注意:fill-mode: forwards只负责在动画结束后应用最后一帧状态,但它不能替代基础样式。如果你希望动画结束后的某个样式在其他场景下依然生效,那就需要把对应样式写到基础类里,而不是完全依赖动画的填充。

3. 动画执行次数与逆向播放

3.1 iteration-count 与方向属性的配合逻辑

animation-iteration-count的取值可以是一个数字,也可以写成infinite,代表无限次执行。默认值是 1。

数字很好理解,但配合animation-direction之后,情况就变得复杂了。

animation-direction有四个值:

  • normal:每次循环都从 0% 到 100% 播放。
  • reverse:每次循环都从 100% 到 0% 播放。
  • alternate:第一次从 0% 到 100%,第二次从 100% 到 0%,如此交替。
  • alternate-reverse:从 100% 到 0% 开始,然后再从 0% 到 100%,与alternate相位相反。

重点是理解“每次循环”的概念。animation-direction: alternate配合animation-iteration-count: 2,实际效果是:第一轮正向播放,第二轮反向播放,动画看起来像是一个来回。这和把动画时长翻倍、再在关键帧里写来回效果,本质区别在于缓动函数的应用。

direction: alternate下,第一次播放使用ease,第二次反向播放时,ease也会以“反向”的方式应用,所以视觉上更柔和。而如果是在一个关键帧里做往返,缓动只会应用在整个线性过程上,无法针对往返两端分别控制。这是很多动效卡顿或不自然的隐藏原因。

3.2 实现“循环播放”时的状态衔接技巧

循环播放最简单的场景是“转圈”,但真实项目中更多的是“脉冲、心跳、呼吸”这类效果,它们的核心要求是循环首尾状态要平滑衔接。

以呼吸灯效果为例,我通常会写一组关键帧:

@keyframes breathe { 0%, 100% { transform: scale(1); opacity: 0.6; } 50% { transform: scale(1.05); opacity: 1; } }

注意这里用了0%, 100%双起始点写法,让动画的起点和终点是同一个状态。这种写法配合infinite无限循环时,循环接缝处几乎察觉不到跳变。

如果要更自然的来回效果,我通常直接这样配置:

.element { animation: breathe 2s ease-in-out infinite alternate; }

alternate让这个动画自动来回播放,整个过程无需手动写往返两个方向的关键帧,实现成本大幅降低。

3.3 用alternateinfinite做往返动画的实战示例

下面是一个真实可跑的加载提示动画。圆点从左移到右,然后自动返回,无限循环:

.dot { width: 12px; height: 12px; background: #409eff; border-radius: 50%; animation: slideX 1.2s ease-in-out infinite alternate; } @keyframes slideX { from { transform: translateX(0); } to { transform: translateX(48px); } }

这里通过alternate,不需要在关键帧里写 0%/100% 的往返过程,一次往返由两次迭代自动完成,代码简洁,而且延迟和缓动的效果也更自然。

需要注意:在alternate模式下,animation-iteration-count: infinite会反复正播、反播。如果你数了播放次数,会发现每次往返算两次迭代。这对绝大多数场景没有影响,但在某些需要精准控制总次数的场景下(比如动画只播放 3 次),要理解 3 次迭代等于“正、反、正”的视觉变化。

4. 组合使用四个属性的完整流程

4.1 步骤拆解:从设计到落地的完整示例

现在用一个具体案例来串联前面所有知识点。

需求:页面加载后,一个提示卡片在 800ms 后从下方滑入,滑入后保持可见;3 秒后卡片开始上下浮动,无限循环。

先拆分成两个独立动画:

第一个是进场动画“滑入”,它只执行一次,结束后要保持在结束位置。所以需要animation-fill-mode: both,延迟 800ms。

第二个是浮动动画,它要等进场动画结束后才开始,无限循环。用一个.float动画,配infinite

整体控制方式有两种。一种是给同一个元素写两个动画:

.card { animation: slideIn 0.6s ease-out 0.8s both, float 2s ease-in-out 3s infinite; }

通过逗号分隔,可以让一个元素同时执行多个动画。注意:多个动画的延迟和次数是独立控制的,所以第二个动画设置了3s延迟,等待进场动画结束。

这时有一个关键细节:animation-fill-mode: both只作用于它对应的那一个动画。第一个动画配置了both,第二个动画没有配置,所以第二个动画在等待期间(3s 延迟)不会影响元素状态。

4.2 参数计算:延迟、时长与循环次数的推算逻辑

拿上面例子来说,进场动画 0.6 秒,延迟 0.8 秒,结束时间在 1.4 秒。浮动动画延迟 3 秒,意味着进场结束后还有 1.6 秒的静止状态。如果想无缝衔接,浮动动画的延迟时间应该按进场动画时长 + 进场动画延迟 = 0.6 + 0.8 = 1.4s来算。

有一个实用思路:在做多段动画时,一定要画一条简单的时间轴,把每个动画的开始时间、结束时间标出来。否则全靠肉眼看效果,很容易出现动画之间有空档或者重合的情况。

这里给出一个参考计算表:

动画名时长延迟开始时间结束时间
slideIn0.6s0.8s0.8s1.4s
float2s1.4s1.4s持续循环

看到没,第二个动画的延迟正好是第一个动画的结束时间,这样就能实现“进场完成、马上开始浮动”的连续动效。

4.3 多动画叠加的优先级与命名规范

多个动画叠加在一个元素上时,如果两个动画同时改变同一个属性(比如transform),后声明的动画生效优先级更高。实际测试下来,后面声明的动画会覆盖前面动画对该属性的控制

所以如果你在进场动画里改变了transform: translateY(...),浮动动画里也改变了transform: translateY(...),一旦浮动动画开始,它就会覆盖进场动画最终保持的位置,出现瞬间跳动。

解决方式是把两个动画控制的属性拆开,避免冲突。比如进场动画只控制opacitytransform的位移,浮动动画只控制transform的另一个维度,或者干脆给浮动动画内部的关键帧也加上位移偏移,让它包含起始状态的位置。

我在项目里的命名习惯是:enter-开头表示进场动画,loop-开头表示循环动画,exit-开头表示退场动画。这样一眼就能看出每个动画的使用场景和生命周期,避免误用。

5. 常见问题与排查技巧实录

5.1 动画结束后为什么跳回原位

这个问题基本上就是animation-fill-mode设置不对。默认是none,动画结束后元素回到初始默认状态。

处理方式有几类:

  • 如果只是想让动画结束后留在最后状态,用forwards
  • 如果需要延迟期也应用关键帧首帧,用backwards
  • 大多数进场动画直接用both最省事。

排查时需要检查简写语法:animation: fadeIn 2s both;这里的both会被正确解析为animation-fill-mode。但如果你写成了animation: fadeIn 2s forwards;并且还单独定义了animation-fill-mode: both,那forwards会被both覆盖,因为后面的声明优先。

5.2 动画只执行一次,未正常循环

这个现象常见于animation-iteration-count写错了位置,或者在简写属性里被覆盖,或者父容器的display变化导致动画重新初始化。

排查顺序:

  1. 确认animation-iteration-count: infinite是否存在。
  2. 检查是否有其他animation简写声明覆盖了该属性。
  3. 如果动画绑定在动态插入的 DOM 上,确认插入后类名是否被移除或重置。

一个很容易踩的坑是:动态删除再添加类名时,浏览器会重新触发动画。如果原来的类名带有animation-iteration-count: infinite,但在某个逻辑分支里被去掉了,动画会在当前状态停住并重置,表现为“无限循环失效”。

5.3 动画延迟期间元素状态错误

配合热词“css3动画延迟和完成后状态的保持”,这类问题集中在animation-delayfill-mode的配合上。

假设你要做一个 toast 提示:出现之后 2s 才消失。结构通常是:

.toast { animation: fadeIn 0.3s ease 0.5s both, fadeOut 0.3s ease 2s both; }

这里如果fadeIn没有both,就会在 0.5s 延迟期间先显示元素原始状态,然后动画突然从透明变成不透明,产生闪烁。

解决方式就是在fadeInfadeOut上都加both,保证延迟期和结束期都处于正确状态。

经验补充:我建议在 CSS 动画中凡是涉及透明度和位移的进出场效果,都习惯性加上both。虽然看起来冗余,但能省掉绝大多数由于状态复位引起的视觉 bug。

5.4 动画卡顿或闪烁的常见原因

CSS3 动画出现卡顿或闪烁,最常见的原因有两个:一是动了会引起布局抖动的属性,二是没触发 GPU 加速。

优先使用transformopacity来做动画。动画位移不要用topleftmargin这些属性,它们会引起重排,性能开销很大。

如果某个元素动画出现锯齿或闪烁,试着加一句:

.element { transform: translateZ(0); backface-visibility: hidden; }

这能强制开启 GPU 加速,缓解闪烁问题。但要注意,这只是治标手段,不能过度依赖。大量元素同时开启 GPU 加速,反而会占用过多显存,造成新的卡顿。

5.5 问题排查速查表

现象大概率原因处理方式
动画结束跳回原位fill-mode 为 none改为 forwards 或 both
延迟期间元素状态错误缺少 backwards/both加上 fill-mode: both
动画不循环,只播一次iteration-count 写错或覆盖显式声明 infinite
来回播放不自然direction 设错改用 alternate
动画卡顿使用了非合成属性改用 transform/opacity
动画闪烁GPU 加速未开启适当使用 translateZ(0)
多个动画相互覆盖属性冲突拆开不同属性或错开时间

5.6 独家避坑心得

写 CSS 动画这几年,我总结出几条避坑法则。

第一条,能不简写就不简写。尤其在属性多的时候,显式声明会让代码可读性高很多,也不会出现简写覆盖的问题。简写在大多数场景下是个省时工具,但在调试动画时,直接声明生效值会省很多查错时间。

第二条,把所有动画做成时间轴的思维。先画出每个动画的起始时间和持续时长,再写代码,而不是一遍遍刷新浏览器调参。虽然看起来慢,但一次调对的概率非常高。

第三条,动画结束后的“善后”要主动做。比如一个提示框在fadeOut结束后,虽然视觉上已经透明,但它仍然占据布局空间,还可能拦截事件。你需要配合visibility: hidden或在 JS 的animationend事件里给它加上display: none或移除节点。

element.addEventListener('animationend', function () { this.style.display = 'none'; });

这样才不会出现“看不见但点不到”的幽灵元素。

6. 目前踩过的坑,以及我建议的写法

6.1 简化代码时保留必要的完整写法

刚开始写动画时,我也喜欢把所有属性塞到一行animation里。后来维护同事的代码,看到一串animation: slide 0.6s ease-in-out 0.8s infinite alternate both;,两眼一黑,完全看不出哪个值对应哪个属性。

建议在维护性优先的项目里,把动画声明拆开写:

.card { animation-name: slide; animation-duration: 0.6s; animation-timing-function: ease-in-out; animation-delay: 0.8s; animation-iteration-count: infinite; animation-direction: alternate; animation-fill-mode: both; }

行数是多了,但任何一个属性的调整都非常直观。代码是写给同事和未来的自己看的,这一点在复杂动效项目里尤其重要。

6.2 将可复用动画整理成工具类

另一个实用习惯是把高频动画抽成工具类,在项目里直接复用。

@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes slideUp { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: translateY(0); } } .anim-fade-in { animation: fadeIn 0.3s ease both; } .anim-slide-up { animation: slideUp 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; }

这样有几个好处:一是如果产品要求统一动效风格,只需修改关键帧或工具类即可全局生效;二是新页面开发时直接套用类名,不用重新写动画定义,极大提升效率。

6.3 从动画事件到性能的进阶方向

本篇文章解决的是 CSS3 动画在状态保持、延迟、循环、方向四个维度的核心问题,但在真实项目中还会遇到更多交互相关的动画需求。比如在动画结束或开始的时候执行 JavaScript 逻辑,需要监听animationstartanimationendanimationiteration事件;比如需要在某个时间点暂停动画再恢复,需要借助animation-play-state

如果继续深入,你还会接触到steps()缓动函数与逐帧动画、prefers-reduced-motion无障碍支持,以及Web Animations API对动画的脚本化控制,这些在后面的系列里都会逐一展开。

CSS3 动画看起来简单,真正用好的关键在于把每个子属性的行为吃透,尤其是它们之间的边界与配合。这篇的核心,就是帮你把animation-delayanimation-fill-modeanimation-iteration-countanimation-direction这四个最常踩坑的属性一次理清,后续做复杂动效时思路会清晰很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询