CSS 动画:打造流畅的关键帧动画
电影不是静止的画面,而是每秒 24 帧图像连续播放形成的流动影像。CSS Animation(动画)运用同样的原理——通过定义关键帧(Keyframes),让元素在多个状态之间流畅过渡。与 Transition(过渡)只能在两个状态间变化不同,Animation 可以创建包含多个步骤的复杂动画,无需用户交互就能自动播放,是打造加载指示器、装饰效果、引导动画的核心工具。
Animation vs Transition:何时使用哪个?
在学习动画之前,先理解它与过渡的区别:
| 特性 | Transition(过渡) | Animation(动画) |
|---|---|---|
| 触发方式 | 需要状态变化(如 hover) | 自动播放,无需触发 |
| 控制点 | 只有起点和终点 | 可定义多个关键帧 |
| 循环播放 | 不支持 | 支持无限循环 |
| 方向控制 | 只能正向或反向 | 支持来回播放、反向等 |
| 暂停/恢复 | 不支持 | 支持 |
| 适用场景 | 交互反馈(按钮、链接) | 持续动画(加载、装饰) |
简单判断:
- 用户触发的简单效果 → Transition
- 自动播放或复杂多步骤 → Animation
@keyframes:定义动画序列
@keyframes 规则定义了动画在时间轴上的关键帧,每个关键帧描述元素在该时刻的样式。
基础语法
/* 定义一个名为 slidein 的动画 */
@keyframes slidein {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* 使用百分比表示更精确的时间点 */
@keyframes fadeInOut {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}- from 等同于 0%(动画开始)
- to 等同于 100%(动画结束)
- 可以定义任意百分比的关键帧(如 25%、33.33%、75%)
多属性关键帧
每个关键帧可以包含多个 CSS 属性。
@keyframes complexAnimation {
0% {
transform: translateX(0) rotate(0deg) scale(1);
background-color: #3498db;
opacity: 0.5;
}
50% {
transform: translateX(100px) rotate(180deg) scale(1.5);
background-color: #e74c3c;
opacity: 1;
}
100% {
transform: translateX(0) rotate(360deg) scale(1);
background-color: #2ecc71;
opacity: 0.5;
}
}相同样式的关键帧
多个时间点可以共享样式,用逗号分隔。
@keyframes pulse {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}Animation 属性:控制动画播放
定义好关键帧后,使用 animation 属性将其应用到元素上。
animation-name:动画名称
指定要使用的 @keyframes 名称。
.box {
animation-name: slidein;
}animation-duration:动画时长
动画完成一个周期所需的时间。
.box {
animation-duration: 2s; /* 2秒 */
}
.fast-animation {
animation-duration: 500ms; /* 0.5秒 */
}animation-timing-function:缓动函数
控制动画的速度曲线,与 transition-timing-function 相同。
.linear {
animation-timing-function: linear; /* 匀速 */
}
.ease {
animation-timing-function: ease; /* 慢-快-慢(默认) */
}
.ease-in-out {
animation-timing-function: ease-in-out; /* 慢速开始和结束 */
}
.custom {
animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
/* 弹跳效果 */
}注意:缓动函数应用于每两个关键帧之间,而不是整个动画。
animation-delay:延迟开始
动画开始前的等待时间。
.delayed {
animation-delay: 1s; /* 1秒后开始 */
}
.negative-delay {
animation-delay: -1s; /* 负值:跳过前1秒,直接从中间开始 */
}animation-iteration-count:播放次数
动画重复的次数。
.play-once {
animation-iteration-count: 1; /* 播放1次(默认) */
}
.play-three-times {
animation-iteration-count: 3; /* 播放3次 */
}
.loop-forever {
animation-iteration-count: infinite; /* 无限循环 */
}
.fractional {
animation-iteration-count: 2.5; /* 播放2.5次 */
}animation-direction:播放方向
控制动画是正向、反向还是来回播放。
.normal {
animation-direction: normal;
/* 每次都从 0% 到 100%(默认) */
}
.reverse {
animation-direction: reverse;
/* 每次都从 100% 到 0% */
}
.alternate {
animation-direction: alternate;
/* 奇数次正向(0%→100%),偶数次反向(100%→0%) */
}
.alternate-reverse {
animation-direction: alternate-reverse;
/* 奇数次反向,偶数次正向 */
}实用场景:alternate 配合 infinite 可创造来回摆动效果。
animation-fill-mode:填充模式
控制动画开始前和结束后元素的样式。
.none-fill {
animation-fill-mode: none;
/* 默认:动画结束后恢复原始样式 */
}
.forwards {
animation-fill-mode: forwards;
/* 保持最后一帧的样式(100% 或 0% 取决于方向) */
}
.backwards {
animation-fill-mode: backwards;
/* 延迟期间应用第一帧样式 */
}
.both {
animation-fill-mode: both;
/* 同时应用 forwards 和 backwards */
}常用:forwards 让元素保持动画结束时的状态。
animation-play-state:播放/暂停
控制动画是播放还是暂停。
.running {
animation-play-state: running; /* 播放(默认) */
}
.paused {
animation-play-state: paused; /* 暂停 */
}
/* 实际应用:悬停时暂停 */
.animation-box {
animation: spin 2s linear infinite;
}
.animation-box:hover {
animation-play-state: paused;
}animation 简写属性
实际开发中更常用简写形式:
/* 语法:name duration timing-function delay iteration-count direction fill-mode play-state */
.box {
animation: slidein 2s ease-in-out 0.5s infinite alternate both running;
}
/* 常用简化版 */
.box {
animation: slidein 2s infinite;
}
/* 多个动画,用逗号分隔 */
.box {
animation: slidein 2s, fadeIn 1s 0.5s, rotate 3s infinite;
}顺序提示:第一个时间值是 duration,第二个是 delay。
实际应用:常见动画效果
加载指示器:旋转圆圈
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}脉冲效果
@keyframes pulse {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.7;
}
}
.pulse-button {
animation: pulse 2s ease-in-out infinite;
}弹跳动画
@keyframes bounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-30px);
}
60% {
transform: translateY(-15px);
}
}
.bounce-element {
animation: bounce 2s infinite;
}淡入淡出
@keyframes fadeInOut {
0%,
100% {
opacity: 0;
}
50% {
opacity: 1;
}
}
.fade-text {
animation: fadeInOut 3s infinite;
}滑入效果
@keyframes slideInLeft {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.slide-in {
animation: slideInLeft 0.5s ease-out forwards;
}摇晃效果
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
10%,
30%,
50%,
70%,
90% {
transform: translateX(-10px);
}
20%,
40%,
60%,
80% {
transform: translateX(10px);
}
}
.shake-on-error {
animation: shake 0.5s;
}打字机效果
@keyframes typing {
from {
width: 0;
}
to {
width: 100%;
}
}
@keyframes blink {
50% {
border-color: transparent;
}
}
.typewriter {
width: 0;
overflow: hidden;
white-space: nowrap;
border-right: 3px solid #333;
animation: typing 3.5s steps(40) 1s forwards, blink 0.75s step-end infinite;
}实战案例:复杂加载动画
创建一个多点跳动的加载指示器。
<div class="loader">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>@keyframes bounce {
0%,
80%,
100% {
transform: scale(0);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.loader {
display: flex;
gap: 10px;
justify-content: center;
align-items: center;
height: 100px;
}
.dot {
width: 15px;
height: 15px;
background: #3498db;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
}
/* 错开动画时间 */
.dot:nth-child(1) {
animation-delay: -0.32s;
}
.dot:nth-child(2) {
animation-delay: -0.16s;
}
.dot:nth-child(3) {
animation-delay: 0s;
}实战案例:进度条动画
一个填充的进度条。
<div class="progress-container">
<div class="progress-bar"></div>
</div>@keyframes fillProgress {
from {
width: 0%;
}
to {
width: 100%;
}
}
.progress-container {
width: 100%;
height: 30px;
background: #ecf0f1;
border-radius: 15px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #3498db, #2ecc71);
border-radius: 15px;
animation: fillProgress 3s ease-out forwards;
}实战案例:浮动图标
创建一个上下漂浮的图标。
@keyframes float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-20px);
}
}
@keyframes floatShadow {
0%,
100% {
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
50% {
box-shadow: 0 15px 25px rgba(0, 0, 0, 0.1);
}
}
.floating-icon {
width: 60px;
height: 60px;
background: #3498db;
border-radius: 50%;
animation: float 3s ease-in-out infinite, floatShadow 3s ease-in-out infinite;
}实战案例:卡片翻转动画
自动翻转的卡片。
<div class="auto-flip-card">
<div class="card-inner">
<div class="card-front">Front</div>
<div class="card-back">Back</div>
</div>
</div>@keyframes flip {
0%,
45% {
transform: rotateY(0deg);
}
50%,
95% {
transform: rotateY(180deg);
}
100% {
transform: rotateY(360deg);
}
}
.auto-flip-card {
width: 200px;
height: 300px;
perspective: 1000px;
}
.card-inner {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
animation: flip 6s infinite;
}
.card-front,
.card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
border-radius: 12px;
}
.card-front {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.card-back {
background: linear-gradient(135deg, #f093fb, #f5576c);
color: white;
transform: rotateY(180deg);
}组合多个动画
一个元素可以同时运行多个动画。
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes colorChange {
0% {
background: #3498db;
}
50% {
background: #e74c3c;
}
100% {
background: #2ecc71;
}
}
@keyframes scale {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
}
.multi-animation {
animation: spin 3s linear infinite, colorChange 6s ease-in-out infinite,
scale 2s ease-in-out infinite;
}注意:如果多个动画修改同一属性(如 transform),后面的会覆盖前面的。需要将它们合并到一个 @keyframes 中。
JavaScript 控制动画
通过 JavaScript 可以动态控制动画。
const element = document.querySelector(".box");
// 添加动画
element.style.animation = "slidein 2s ease-out forwards";
// 移除动画
element.style.animation = "none";
// 暂停动画
element.style.animationPlayState = "paused";
// 恢复动画
element.style.animationPlayState = "running";
// 监听动画事件
element.addEventListener("animationstart", () => {
console.log("Animation started");
});
element.addEventListener("animationiteration", () => {
console.log("Animation iteration");
});
element.addEventListener("animationend", () => {
console.log("Animation ended");
});性能优化与最佳实践
优先使用 transform 和 opacity
/* ❌ 低性能:触发重排/重绘 */
@keyframes badAnimation {
from {
width: 100px;
height: 100px;
left: 0;
}
to {
width: 200px;
height: 200px;
left: 100px;
}
}
/* ✅ 高性能:只触发合成 */
@keyframes goodAnimation {
from {
transform: scale(1) translateX(0);
opacity: 1;
}
to {
transform: scale(2) translateX(100px);
opacity: 0.5;
}
}减少动画的元素数量
/* ⚠️ 数百个元素同时动画会卡顿 */
.many-items {
animation: complex 2s infinite;
}
/* ✅ 只在必要的元素上使用动画 */
.featured-item {
animation: highlight 2s infinite;
}使用 will-change
.heavy-animation {
will-change: transform, opacity;
animation: complexMove 2s infinite;
}控制帧率:使用 steps()
对于精灵图动画,使用 steps() 代替平滑过渡。
@keyframes spriteAnimation {
to {
background-position: -480px 0; /* 假设有8帧,每帧60px */
}
}
.sprite {
width: 60px;
height: 60px;
background: url("sprite.png") no-repeat;
animation: spriteAnimation 0.8s steps(8) infinite;
/* 8步,每帧显示0.1秒 */
}常见问题与解决方案
问题 1:动画在页面加载时立即播放
有时希望用户看到某个状态后再开始动画。
/* 使用 animation-delay */
.delayed-start {
animation: fadeIn 1s 2s forwards;
/* 延迟2秒后开始 */
}
/* 或者用 JavaScript 控制 */setTimeout(() => {
document.querySelector(".box").classList.add("animated");
}, 2000);问题 2:动画结束后闪烁
使用 animation-fill-mode: forwards 保持最后一帧。
.fade-out {
animation: fadeOut 1s forwards; /* 不会跳回初始状态 */
}问题 3:无限动画占用资源
在不可见时暂停动画。
document.addEventListener("visibilitychange", () => {
const animations = document.querySelectorAll(".animated");
animations.forEach((el) => {
el.style.animationPlayState = document.hidden ? "paused" : "running";
});
});问题 4:动画在移动端不流畅
/* 添加硬件加速 */
.smooth-animation {
transform: translate3d(0, 0, 0); /* 触发GPU加速 */
will-change: transform;
animation: move 2s infinite;
}可访问性考虑
某些用户可能对动画敏感,应尊重系统设置。
/* 检测用户是否偏好减少动画 */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* 或者只关闭特定动画 */
@media (prefers-reduced-motion: reduce) {
.decorative-animation {
animation: none;
}
.functional-animation {
animation-duration: 0.3s; /* 保留但缩短 */
}
}总结
CSS Animation 是创造动态、引人注目视觉体验的强大工具:
核心概念:
- @keyframes:定义动画序列的关键帧
- animation-name:指定要使用的动画
- animation-duration:动画时长
- animation-iteration-count:播放次数(可无限循环)
- animation-direction:播放方向
- animation-fill-mode:动画前后的状态
最佳实践:
- 性能优先:使用
transform和opacity - 适度使用:不是所有元素都需要动画
- 考虑可访问性:尊重
prefers-reduced-motion - 语义明确:动画名称应描述其效果
- 测试性能:在低端设备上验证流畅度
何时使用 Animation:
- 加载指示器(旋转、脉冲、进度条)
- 装饰性动画(背景图案、漂浮元素)
- 引导动画(箭头指示、高亮提示)
- 品牌动效(Logo 动画、标志性效果)
CSS 动画让静态网页活了起来,但记住——动画是服务于用户体验的工具,而不是目的本身。最好的动画是那些增强理解、提供反馈、引导注意力的动画,而不是那些分散注意力、炫耀技巧的动画。优雅的动画应该像呼吸一样自然,用户会感受到它的存在,但不会被它打扰。