CSS 教程 / CSS 关键帧动画:填充模式、方向与暂停

CSS 关键帧动画描述在动画时间线上的属性值,并且可以在没有悬停或聚焦触发的情况下运行。持续时间、延迟、迭代次数、填充模式和播放状态控制该行为的不同部分。

区分过渡和关键帧动画

标准 过渡 动画
开始条件 当数值因悬停、聚焦、类更改等而发生变化时。 在应用动画并且延迟时间结束后,
状态定义 变化前后的 CSS 值 在关键帧中指定起始、中间和结束值
重复次数和方向 进行状态更改 单独指定次数、方向和停止状态

如果只是按钮背景色随状态改变,transition 通常已经足够。你可以查看动画效果,例如通知首次出现时的效果,或者通过多个步骤进行的可视化说明。同时用两个函数控制同一元素的同一属性会很难跟踪哪个值被应用,所以在这个练习中我们分开了角色。如果状态变化前后的效果已经足够,可以考虑使用 CSS 变换和过渡 来从简单的过渡开始。

定义关键帧和播放设置

在关键帧中,from 对应 0%,to 对应 100%。如果需要中间状态,可以添加一个进度百分比,例如 50%。仅声明关键帧名称不会使其播放;必须将其与元素的 animation-name 关联。

@keyframes travel {
  0% { transform: translateX(0); }
  50% { transform: translateX(3rem); }
  100% { transform: translateX(6rem); }
}
.marker { animation: travel 4s linear 1s 1 normal forwards; }

这段部分代码是下面完整示例中同名关键帧和 .marker 设置的简化版本。4s 是播放一次所需的时间,1s 是启动延迟。如果未指定持续时间,默认值为 0 秒,这会使观察运动过程变得困难。最初可以通过将其分解为长格式属性来减少对时间和频率的混淆。

属性 此示例的值 需要更改的内容
animation-duration 4s 一次播放时间
animation-timing-function linear 固定进度与缓动曲线的区别
animation-delay 1s 开始前的等待时间
animation-iteration-count 1 播放次数;infinite 表示无限重复
animation-direction normal 正向/反向/交替播放
animation-fill-mode forwards 等待播放和播放结束后应用的样式值
animation-play-state 默认 running 用 paused 暂停当前进度

关键帧的百分比间隔相等,并不保证各段运动速度相同;还要考虑属性值的差异和缓动函数。本练习使用 linear,这使得检查中间位置变得容易。有关关键帧的创建和连接,请参考 MDN:@keyframes 。

比较方向、迭代次数和填充模式

normal 从开始到结束进行,而 reverse 从结束到开始进行。alternate 首次以正向播放,下次以反向播放交替进行。如果迭代次数为 2 且交替进行,则最后一次将到达第一个关键帧。

forwards 不是一个始终保持 100% 关键帧的设置。根据播放方向和次数,会保持最后实际到达的关键帧的值。在示例中,正向播放一次保持 6rem 的位置,反向播放一次保持 0 的位置,交替播放两次也保持 0 的位置。此描述基于整数次数的完整播放,分数次数可能会在中途中结束。MDN:animation-fill-mode 的方向与结束状态 。

fill-mode 延迟 完成后
none 基础 CSS 基础 CSS
forwards 基础 CSS 最后播放的关键帧
backwards 本次方向下的起始关键帧值 基础 CSS
both 本次方向下的起始关键帧值 最后播放的关键帧

如果基础 CSS 值和结束关键帧值相同,none 和 forwards 之间的差异可能不可见。下面的移动框默认位置为 1rem,起始为 0,结束为 6rem,以显示差异。另一方面,必读指南默认为动画移除时显示的完成状态。

运行并一次更改一个设置

将以下代码保存为 UTF-8 格式的 animation-lab.html,并在浏览器中打开。上方说明经过 400ms 入场动画后持续可见,下面的装饰框在 1 秒后开始移动,持续移动 4 秒。修改值后,保存文件并重新加载页面,从头进行比较。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Keyframes and end states exercise</title>
  <style>
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; padding: 1rem; color: #183044; font-family: sans-serif; line-height: 1.6; }
main { max-width: 46rem; margin-inline: auto; }
h1 { font-size: 1.5rem; } h2 { font-size: 1.2rem; }
button, input { font: inherit; }
button { cursor: pointer; }
:focus-visible { outline: 3px solid #164f73; outline-offset: 3px; }
.notice { padding: 1rem; border: 1px solid #90bdd8; border-radius: .5rem; background: #eaf4fa;
  opacity: 1; transform: none; animation: appear 400ms ease-out; }
@keyframes appear {
  from { opacity: 0; transform: translateY(.5rem); }
  to { opacity: 1; transform: translateY(0); }
}
.track { margin-block: 1rem; padding: 1rem; border: 2px dashed #8a9ba7; }
.marker { width: 2rem; height: 2rem; border-radius: .25rem; background: #166b92;
  transform: translateX(1rem);
  animation-name: travel; animation-duration: 4s; animation-timing-function: linear;
  animation-delay: 1s; animation-iteration-count: 1; animation-direction: normal;
  animation-fill-mode: forwards; }
@keyframes travel {
  0% { transform: translateX(0); background-color: #166b92; }
  50% { transform: translateX(3rem); background-color: #6a3989; }
  100% { transform: translateX(6rem); background-color: #28633b; }
}
#pause:checked ~ .track .marker { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .notice, .marker { animation: none; }
}
  </style>
</head>
<body>
<main>
<h1>Check entrance and movement</h1>
<p class="notice">This notice remains readable without animation.</p>
<h2>Comparing end states</h2>
<p>Movement starts one second after loading. Refresh the page to compare again.</p>
<input id="pause" type="checkbox"><label for="pause">Pause movement</label>
<div class="track"><div class="marker" aria-hidden="true"></div></div>
<p>The colors and movement are decorative. Essential information remains available independently of the animation, as in this sentence.</p>
</main>
</body>
</html>
  1. 在默认设置下,盒子在延迟期间位置为1rem,开始时移动到0,并在结束时保持在位置6rem。检查延迟值与起始值之间的差异。
  2. 只将animation-fill-mode更改为none并刷新。完成后,它将返回默认的1rem位置和默认的蓝色。恢复到 forwards。
  3. 只更改 fill-mode 为 both。即使在延迟期间,起始位置0也适用。确认后,返回 forwards。
  4. 保持 animation-iteration-count 为 1并将方向更改为reverse。完成后,检查0位置是否保持不变,并恢复为 normal。
  5. 通过将 animation-direction 改为 alternate,并保持次数为 1来检查结果。在该状态下,如果将数字更改为2,将会进行第二次播放。确认后,计数为1,方向恢复为 normal。
  6. 刷新后立即勾选 Pause movement 复选框并稍等片刻。如果取消,将从中断的地方继续。这不同于从头重新播放的功能。

保持内容可见并减少运动

本例在减少动态效果条件匹配时,通过 animation: none 移除说明和装饰框的动画。说明仍然可读,因为默认设置是 opacity: 1;下方装饰框则回到默认的 1rem 位置。仅在结束关键帧或 forwards 中设置可读状态可能导致在没有动画的情况下内容消失。

在 DevTools 渲染面板或操作系统中启用减少动画,并重新加载。两个元素在内容仍可见时应保持静止。请参考 MDN:prefers-reduced-motion,然后在比较后恢复模拟设置。还要遵循 使用宽度、缩放和键盘测试检查响应式 CSS,以在禁用动画时检查裁剪和可用性。

在添加无限重复之前,先决定为什么需要运动以及它将如何停止。此示例使用有限的迭代,并独立于运动和颜色提供重要信息。如果没有任何运动,请检查动画名称、持续时间和减少运动的条件。对于意外的结束状态,请比较方向、迭代次数、填充模式以及底层 CSS。