一、动画:让界面“活”起来的最后一块拼图
前面几篇,我们依次拆解了 CSS 的选择器、属性值确定机制和布局系统。现在来到 CSS 核心机制的最后一环——动画。
动画不是“锦上添花”的装饰品。在用户体验中,动画承担着关键功能:
- 提供反馈:按钮被按下时微微下沉,让用户知道操作已生效。
- 引导注意力:新消息弹出时带一个弹入效果,把用户的目光吸引过去。
- 建立空间感:页面切换时的滑动效果,帮用户理解“我从哪来,到哪去”。
- 掩盖加载时间:骨架屏的闪烁动画,让等待不那么焦虑。
CSS 提供了两套动画机制:过渡和关键帧动画。前者适合简单的“从 A 到 B”的变化,后者适合复杂的、多阶段的动画序列。理解两者的适用场景和底层原理,是写出流畅动画的前提。
二、过渡:让变化变得平滑
过渡是最简单的动画形式。它告诉浏览器:“当这个属性发生变化时,不要瞬间切换,而是在一段时间内平滑过渡。”
基本语法
transition: 属性名 持续时间 缓动函数 延迟时间;
四个子属性:
| 属性 | 作用 | 示例 |
|---|---|---|
transition-property |
要过渡的 CSS 属性 | opacity、transform、background |
transition-duration |
过渡持续时间 | 0.3s、500ms |
transition-timing-function |
缓动函数,控制变化速率 | ease(默认)、linear、ease-in-out |
transition-delay |
延迟多久后开始过渡 | 0s(默认)、0.2s |
最简单的示例:hover 时按钮变色
button {
background: #4a90d9;
transition: background 0.3s ease;
}
button:hover {
background: #357abd;
}
当鼠标移入按钮时,背景色从蓝色平滑过渡到深蓝色,耗时 0.3 秒。鼠标移出时,同样平滑过渡回蓝色。不需要写任何 JavaScript。
过渡多个属性
.card {
background: white;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transform: translateY(0);
transition: background 0.3s, box-shadow 0.3s, transform 0.3s;
}
.card:hover {
background: #f8f9fa;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
transform: translateY(-4px);
}
鼠标悬停时,卡片的背景、阴影、位置同时平滑变化,制造“浮起来”的立体效果。
过渡的触发条件
过渡发生在属性值改变时。常见的触发方式:
- 伪类状态变化:
:hover、:focus、:active。 - JavaScript 修改样式:
element.style.opacity = '0'会触发 opacity 的过渡。 - 类名变化:JS 给元素添加或移除 class,导致样式变化。
过渡的局限:它只能做“从 A 到 B”的简单动画。如果需要多阶段(先变大再变小再弹回来),或者需要动画自动开始(不依赖用户操作),就需要关键帧动画。
三、关键帧动画:掌控每一帧
@keyframes 让你定义动画的多个阶段,每个阶段可以设置不同的样式。然后用 animation 属性把动画应用到元素上。
定义关键帧
@keyframes 动画名 {
0% { /* 动画开始时的样式 */ }
50% { /* 动画进行到一半时的样式 */ }
100% { /* 动画结束时的样式 */ }
}
百分比表示动画的进度。你也可以用 from 和 to 代替 0% 和 100%。
应用动画
.element {
animation: 动画名 持续时间 缓动函数 延迟时间 播放次数 方向 填充模式;
}
八个子属性(分两组记忆):
| 属性 | 作用 | 常用值 |
|---|---|---|
animation-name |
关键帧名称 | 自定义名称 |
animation-duration |
持续时间 | 0.5s、2s |
animation-timing-function |
缓动函数 | ease、linear、cubic-bezier() |
animation-delay |
延迟时间 | 0s、1s(可以为负值,表示从中间开始) |
animation-iteration-count |
播放次数 | 1(默认)、infinite(无限循环)、3 |
animation-direction |
播放方向 | normal、reverse、alternate(来回) |
animation-fill-mode |
动画结束后保持什么状态 | none(默认)、forwards(保持最后一帧)、backwards、both |
animation-play-state |
播放或暂停 | running(默认)、paused |
经典示例:淡入上浮效果
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: fadeInUp 0.5s ease-out both;
}
元素从透明、下方 20px 的位置,淡入并上浮到原位。both 表示:动画开始前就应用第一帧的样式(防止闪烁),动画结束后保持最后一帧的样式。
多个动画同时作用
用逗号分隔多个动画:
.spinner {
animation: spin 1s linear infinite, pulse 2s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
四、缓动函数:动画的“性格”所在
同样是从 A 到 B、同样耗时 0.3 秒,不同的缓动函数会让动画呈现出完全不同的“性格”。
预设的缓动函数
linear:匀速。机械、生硬,自然界几乎不存在匀速运动。ease(默认):慢→快→慢。柔和自然,适合大多数过渡效果。ease-in:慢→快。适合元素退场(慢慢开始,加速消失)。ease-out:快→慢。适合元素入场(快速出现,慢慢停稳)。ease-in-out:慢→快→慢,比ease更对称。适合循环动画。
自定义缓动:cubic-bezier()
所有缓动函数本质都是贝塞尔曲线。cubic-bezier(x1, y1, x2, y2) 让你自定义曲线的形状:
/* 弹性效果 */
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);
四个参数分别控制两个控制点的坐标。你可以在 Chrome 开发者工具中直观地拖拽曲线来调整参数,然后复制生成的 cubic-bezier() 值。
实战建议:不要对所有元素使用完全相同的缓动函数。入口动画用 ease-out(快进慢停),出口动画用 ease-in(慢出快走),状态切换用 ease 或 ease-in-out。
五、性能:为什么 transform 和 opacity 是动画首选
不是所有 CSS 属性都适合做动画。从渲染性能的角度,CSS 属性分为三个梯队。
第一梯队(只触发合成,性能最好)
transform 和 opacity 是仅有的两个不会触发重排或重绘的属性。它们的动画在 GPU 上完成,不占用主线程。
transform: translateX()、translateY()、scale()、rotate():移动、缩放、旋转。opacity:透明度变化。
原则:能用 transform 实现的移动,绝不用 left/top 或 margin。前者性能好得多。
第二梯队(触发重绘,不触发重排)
color、background-color、box-shadow、border-color 等只影响元素外观、不影响布局的属性。它们的动画需要 CPU 重新绘制,但不会重新计算布局。
第三梯队(触发重排,性能最差,应避免)
width、height、margin、padding、left、top、font-size 等影响元素尺寸或位置的属性。修改它们会导致浏览器重新计算布局(reflow),然后重绘(repaint),开销最大。
如果需要改变元素尺寸,考虑用 transform: scale() 代替 width/height 的变化。如果需要移动元素,用 transform: translate() 代替 left/top 或 margin。
性能检查清单
- 动画中使用最多的属性应该是
transform和opacity。 - 避免在动画中修改
width、height、left、top、margin、padding。 - 如果必须在动画中使用第二梯队属性(如
background-color),确保动画元素被提升到独立的合成层:使用will-change: transform或transform: translateZ(0)(但不要滥用)。 - 用 Chrome 开发者工具的 Performance 面板录制动画,检查帧率是否稳定在 60fps。
六、will-change:提前告诉浏览器“我要动”
will-change 属性让你提前通知浏览器某个元素即将发生变化,浏览器可以提前做优化准备(比如将该元素提升到独立的合成层)。
.card {
will-change: transform;
}
使用原则:
- 不要给太多元素加:每个合成层都会消耗 GPU 内存。只给确实会频繁动画的元素加。
- 在动画开始前添加,动画结束后移除:JS 可以在
mouseenter时加will-change,在animationend时移除。 - 不要“以防万一”地加:浏览器有自己的优化策略,滥用
will-change反而可能降低性能。
七、animation-delay 为负值:从中间开始播放
这是一个不太为人知但非常实用的技巧。将 animation-delay 设为负值,可以让动画从一个中间状态开始播放:
/* 动画总时长 2s,delay 为 -1s → 跳过前半段,从 50% 处开始 */
.spinner:nth-child(1) { animation-delay: 0s; }
.spinner:nth-child(2) { animation-delay: -1.5s; }
.spinner:nth-child(3) { animation-delay: -0.3s; }
利用这个技巧,可以让多个相同动画的元素呈现出错落有致的节奏感,而不需要为每个元素定义不同的关键帧。
八、综合演示:一个完整的小动画系统
下面这段代码综合运用了过渡、关键帧动画、缓动函数和性能最佳实践:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>CSS 动画综合演示</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: #f0f4f8;
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
}
.demo-container {
display: flex;
flex-direction: column;
gap: 32px;
width: 400px;
}
/* ==================== 演示一:过渡 ==================== */
.btn {
padding: 14px 32px;
background: #4a90d9;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s ease, transform 0.2s ease, box-shadow 0.3s ease;
}
.btn:hover {
background: #357abd;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(74, 144, 217, 0.4);
}
.btn:active {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(74, 144, 217, 0.2);
}
/* ==================== 演示二:关键帧动画 - 加载动画 ==================== */
.loader-row {
display: flex;
justify-content: center;
gap: 8px;
}
.dot {
width: 12px;
height: 12px;
background: #4a90d9;
border-radius: 50%;
animation: bounce 1.4s ease-in-out infinite both;
}
.dot:nth-child(1) { animation-delay: 0s; }
.dot:nth-child(2) { animation-delay: -0.2s; }
.dot:nth-child(3) { animation-delay: -0.4s; }
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0.6);
opacity: 0.4;
}
40% {
transform: scale(1);
opacity: 1;
}
}
/* ==================== 演示三:卡片入场动画 ==================== */
.card {
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
animation: cardIn 0.6s ease-out both;
}
.card:nth-child(1) { animation-delay: 0.1s; }
.card:nth-child(2) { animation-delay: 0.2s; }
.card:nth-child(3) { animation-delay: 0.3s; }
@keyframes cardIn {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ==================== 演示四:脉冲提示 ==================== */
.badge {
display: inline-block;
width: 10px;
height: 10px;
background: #e74c3c;
border-radius: 50%;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(1.3);
}
}
/* ==================== 演示五:骨架屏 ==================== */
.skeleton {
height: 16px;
background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
border-radius: 4px;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.skeleton-card {
display: flex;
flex-direction: column;
gap: 12px;
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
}
.skeleton-title {
height: 20px;
width: 60%;
}
.skeleton-text {
height: 14px;
width: 100%;
}
.skeleton-text.short {
width: 80%;
}
</style>
</head>
<body>
<div class="demo-container">
<!-- 演示一:按钮过渡 -->
<button class="btn">悬停看我</button>
<!-- 演示二:加载动画 -->
<div class="loader-row">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
<!-- 演示三:卡片入场 -->
<div class="card">
<h3>通知一</h3>
<p>你的文章已发布。</p>
</div>
<div class="card">
<h3>通知二</h3>
<p>你有一条新评论。</p>
</div>
<!-- 演示四:脉冲提示 -->
<div style="display: flex; align-items: center; gap: 8px;">
<span class="badge"></span>
<span style="color: #666; font-size: 14px;">有新消息</span>
</div>
<!-- 演示五:骨架屏 -->
<div class="skeleton-card">
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-text"></div>
<div class="skeleton skeleton-text short"></div>
<div class="skeleton skeleton-text"></div>
</div>
</div>
</body>
</html>
逐块解析:
- 按钮过渡:
hover时背景、阴影、位移三项同时平滑过渡,active时按钮归位。这是微交互的标准写法。 - 加载动画:三个圆点用相同的
@keyframes,但通过不同的负animation-delay制造错落感。这是负 delay 技巧的经典应用。 - 卡片入场:每个卡片依次从下方淡入上浮,通过递增的
animation-delay实现依次出现的效果。both确保动画前后都保持正确的状态。 - 脉冲提示:红色圆点周期性缩放和变透明,模拟“呼吸”效果,用于提示用户注意。
- 骨架屏:用渐变色背景 + 位移动画模拟加载中的占位效果,比静态的灰色方块更有“正在加载”的感觉。这是目前主流的加载占位方案。
九、本篇小结
这一篇我们系统学习了 CSS 的动画机制:
- 过渡:最简单的动画形式,在属性值发生变化时平滑过渡。
transition: 属性 时长 缓动 延迟;。适合 hover 效果、状态切换。 - 关键帧动画:用
@keyframes定义多阶段动画,用animation属性应用。适合加载动画、入场动画、循环动画。 - 缓动函数:决定动画的“性格”。
ease-out适合入场,ease-in适合退场,ease-in-out适合循环。可通过cubic-bezier()自定义。 - 性能最佳实践:动画应优先使用
transform和opacity(只触发合成),避免width/height/left/top(触发重排)。will-change可提前通知浏览器优化,但不应滥用。 - 负 delay 技巧:让动画从中间状态开始,配合不同的 delay 值制造错落节奏。
至此,CSS 的四个核心机制——选择器、属性值确定、布局、动画——全部讲完。从下一篇开始,我们将进入 JavaScript 的深层世界。
下一篇预告
下一篇,我们将开启 JS 篇——《JavaScript 简史与面向对象之道》。你会了解 JavaScript 是如何在 10 天内被创造出来的,它的原型继承机制到底是怎么回事,以及为什么说“JavaScript 中的一切皆对象”这句话既对又不对。
前端,每周更新。













暂无评论内容