4. CSS关键帧动画(Keyframes):@keyframes规则、animation属性、多步骤动画控制

说实话,CSS动画里最让我着迷的就是关键帧动画。为什么?因为它给了你「导演」的权力——你可以精确告诉浏览器,在动画的哪个时间点,元素应该长什么样。

我记得刚入行那会儿,做一个小球弹跳效果,用transition死活调不出那种「砸到地面再弹起」的质感。后来同事甩了一句:「用@keyframes啊,中间加几个关键帧控制速度曲线。」嗯,从那以后,我就再也没用transition做过复杂动画了。

4.1 @keyframes规则:动画的剧本

@keyframes说白了就是一份「时间-状态对照表」。你定义好几个时间节点,每个节点上元素长什么样,浏览器自动帮你算中间过程。

@keyframes bounce {
  0% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-100px);
  }
  100% {
    transform: translateY(0);
  }
}

这里我用了三个关键帧:起点、中间最高点、终点。你想想看,如果只用transition,你只能定义起点和终点,中间过程完全交给浏览器。但用@keyframes,你可以在50%的位置插入一个「弹起」状态,效果完全不一样。

我的习惯:关键帧的百分比不一定要均匀分布。比如做「蓄力-爆发」效果,我会把80%的时间放在蓄力阶段,最后20%瞬间爆发。这种节奏感,只有@keyframes能实现。

4.2 animation属性:把剧本搬上舞台

定义好@keyframes之后,需要用animation属性把它「激活」。这个属性是个简写,背后有8个子属性,但常用的其实就那几个。

.ball {
  animation: bounce 1s ease-in-out infinite;
}

这一行代码干了四件事:引用了bounce这个关键帧,时长1秒,缓动函数ease-in-out,无限循环。

我个人习惯把animation拆开来写,尤其是调试阶段——这样能快速定位问题。

.ball {
  animation-name: bounce;
  animation-duration: 1s;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  animation-direction: alternate;
  animation-fill-mode: both;
  animation-delay: 0.5s;
  animation-play-state: running;
}
子属性 作用 常用值
animation-name 引用@keyframes名称 bounce, fadeIn, 自定义
animation-duration 单次动画时长 0.3s, 1s, 2s
animation-timing-function 速度曲线 ease, linear, cubic-bezier()
animation-iteration-count 播放次数 1, 3, infinite
animation-direction 播放方向 normal, reverse, alternate
animation-fill-mode 动画前后的状态 none, forwards, backwards, both
animation-delay 延迟启动 0s, 0.5s, -1s(提前播放)
animation-play-state 播放/暂停 running, paused
我曾经踩过的坑:animation-fill-mode默认是none,意味着动画结束后元素会跳回初始状态。如果你想让动画停在最后一帧,记得加上forwards。这个bug我排查了半小时,最后发现是fill-mode没设。

4.3 多步骤动画控制:不止起点和终点

真正的动画高手,不会只用0%和100%。多步骤动画才是精髓。

举个例子,做一个「呼吸灯」效果——不是简单的淡入淡出,而是模拟真实呼吸的节奏:快速吸气、缓慢呼气、停顿。

@keyframes breathe {
  0% {
    opacity: 0.3;
    transform: scale(0.95);
  }
  25% {
    opacity: 1;
    transform: scale(1.05);
  }
  50% {
    opacity: 0.8;
    transform: scale(1.02);
  }
  75% {
    opacity: 0.4;
    transform: scale(0.97);
  }
  100% {
    opacity: 0.3;
    transform: scale(0.95);
  }
}

.light {
  animation: breathe 4s ease-in-out infinite;
}

你看,我用了5个关键帧。25%的位置是吸气峰值,50%开始缓慢呼气,75%接近最低点,100%回到起点。整个周期4秒,模拟一次完整的呼吸。

为什么会这样设计?因为真实世界的运动很少是线性的。你想想看,人呼吸的时候,吸气的速度比呼气快,中间还有短暂的停顿。这些细节,只有多步骤关键帧才能还原。

核心思路:关键帧越多,动画越细腻。但也不是越多越好——我一般控制在5-7个关键帧以内。超过这个数,建议用JavaScript或者GSAP来处理。

4.4 实战技巧:让动画更「活」

这里分享几个我常用的技巧,都是项目里摸爬滚打总结出来的。

  • 错开时间:多个元素同时动画时,给每个元素不同的animation-delay,产生「波浪」效果。比如列表项依次出现,每个延迟0.1秒。
  • 组合关键帧:一个元素可以同时应用多个animation,用逗号分隔。比如同时做位移和颜色变化,各自独立控制。
  • 用cubic-bezier调节奏:默认的ease-in-out太「假」了。我经常用cubic-bezier(0.68, -0.55, 0.27, 1.55)做出弹性效果,或者cubic-bezier(0.25, 0.46, 0.45, 0.94)做出自然衰减。
  • 暂停与恢复:用animation-play-state: paused配合:hover,让用户悬停时动画暂停,移开继续。这个交互细节很加分。
.card {
  animation: float 3s ease-in-out infinite;
}

.card:hover {
  animation-play-state: paused;
}

嗯,这里要注意一点:animation-play-state只控制当前动画的暂停和继续,不会重置动画。如果你想让动画重置,需要用JavaScript移除并重新添加类名。

避坑指南:我曾经在移动端做了一个无限循环的旋转动画,结果用户反馈手机发烫。排查后发现是animation-iteration-count: infinite配合高帧率渲染,导致GPU持续高负载。解决方案:非必要不用无限循环,或者降低帧率(用step-start代替平滑过渡)。

最后说一句:CSS关键帧动画虽然强大,但也有边界。如果你需要做复杂的物理模拟、时间线编排、或者交互驱动的动画,还是得上JavaScript。但日常的UI动效——按钮反馈、加载提示、页面过渡——用@keyframes完全够用,而且性能更好。

下一章我会讲CSS过渡动画(Transition),到时候咱们再聊聊「什么时候用Transition,什么时候用Keyframes」这个经典问题。