前言
在现代 Web 开发中,动画早已不是锦上添花的点缀,而是提升用户体验的核心手段之一。无论是按钮反馈、页面转场、加载指示器,还是品牌展示的微交互,流畅自然的动画都能让用户感到界面是"活"的、有反馈的。CSS 提供了两大核心动画方案——过渡(Transition) 和动画(Animation),配合 Transform 变形能力,开发者无需引入 JavaScript 库即可实现绝大多数 UI 动效。
本文将从基础语法出发,覆盖所有核心知识点,并给出大量可直接运行的实战案例,帮助读者系统掌握 CSS 动画技术栈。
一、CSS 过渡(Transition)
CSS 过渡允许我们在 CSS 属性值发生变化时,不是瞬间跳变,而是在一段时间内平滑变化。它是最简单、最常用的 CSS 动画方式。
1.1 过渡的四要素
一个过渡由四个子属性构成:
/* 分别定义 */
.box {
transition-property: background-color;
transition-duration: 0.3s;
transition-timing-function: ease;
transition-delay: 0s;
}
/* 简写形式(推荐) */
.box {
transition: background-color 0.3s ease 0s;
}各属性含义如下:
| 属性 | 含义 | 默认值 |
|---|---|---|
transition-property |
指定要过渡的属性名 | all |
transition-duration |
过渡持续时长 | 0s(无过渡) |
transition-timing-function |
缓动函数,控制变化速率 | ease |
transition-delay |
延迟多久后开始过渡 | 0s |
1.2 过渡多个属性
可以同时过渡多个属性,用逗号分隔:
.card {
width: 200px;
height: 200px;
background: #3498db;
transition: width 0.3s ease,
height 0.3s ease,
background-color 0.5s ease-in;
}
.card:hover {
width: 300px;
height: 300px;
background-color: #e74c3c;
}如果多个属性使用相同的时长和缓动函数,也可以简写为:
.card {
transition: all 0.3s ease; /* 过渡所有可过渡属性 */
}⚠️ 注意:
all虽然方便,但会监听所有属性变化,可能带来不必要的性能开销。建议显式列出需要过渡的属性。
1.3 缓动函数详解
缓动函数决定了动画的加速度曲线,是让动画看起来"自然"的关键。
预定义关键字
/* 五种内置缓动函数 */
.slow-start {
transition-timing-function: ease; /* 慢→快→慢,默认 */
}
.linear-motion {
transition-timing-function: linear; /* 匀速 */
}
.fast-start {
transition-timing-function: ease-in; /* 慢→快 */
}
.fast-end {
transition-timing-function: ease-out; /* 快→慢 */
}
.ease-both {
transition-timing-function: ease-in-out; /* 慢→快→慢,比 ease 更平缓 */
}cubic-bezier() 自定义曲线
cubic-bezier(x1, y1, x2, y2) 接受四个参数,定义一条三次贝塞尔曲线:
.bounce {
/* 回弹效果:超出目标再拉回 */
transition-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.elastic {
/* 弹性伸缩效果 */
transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275);
}常见缓动曲线的贝塞尔值:
| 效果 | cubic-bezier 值 |
|---|---|
| ease | 0.25, 0.1, 0.25, 1.0 |
| linear | 0.0, 0.0, 1.0, 1.0 |
| ease-in | 0.42, 0.0, 1.0, 1.0 |
| ease-out | 0.0, 0.0, 0.58, 1.0 |
| ease-in-out | 0.42, 0.0, 0.58, 1.0 |
💡 在线工具 cubic-bezier.com 可以直观地调试和预览贝塞尔曲线。
steps() 逐帧函数
steps(n, direction) 将过渡分成 n 个离散步骤,而不是平滑过渡:
/* 分成 4 帧,每帧结束时变化 */
.step-anim {
transition-timing-function: steps(4, end);
}
/* 分成 4 帧,每帧开始时变化 */
.step-anim-start {
transition-timing-function: steps(4, start);
}steps() 常配合精灵图(sprite sheet)实现逐帧动画,我们将在实战部分详细展开。
1.4 过渡触发条件
过渡不是自动发生的,它需要属性的值发生变化。常见的触发方式有三种:
伪类触发
.btn {
background: #3498db;
transform: scale(1);
transition: all 0.2s ease;
}
.btn:hover {
background: #2980b9;
transform: scale(1.05);
}
.btn:active {
transform: scale(0.95);
}
.btn:focus {
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.4);
}class 切换
<button onclick="toggleMenu()">菜单</button>
<div id="menu" class="menu">菜单内容</div>
<style>
.menu {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.menu.open {
max-height: 500px; /* 足够大的值 */
}
</style>
<script>
function toggleMenu() {
document.getElementById('menu').classList.toggle('open');
}
</script>JavaScript 直接修改样式
const el = document.querySelector('.box');
el.style.opacity = '0';
// 触发过渡(需要先读取一次布局值,避免合并)
requestAnimationFrame(() => {
el.style.opacity = '1';
});1.5 可过渡属性列表
并非所有 CSS 属性都可以过渡。可过渡属性通常满足以下条件:
- 数值型:如
width、height、margin、padding、font-size - 颜色型:如
color、background-color、border-color - 变换型:如
transform、opacity - 部分其他:如
visibility(离散过渡)、z-index(仅在整数之间)
常见不可过渡属性:
display(只能用 JS 配合 setTimeout/requestAnimationFrame 模拟)background-image(除非使用渐变)font-familypositionfloat
1.6 height: auto 不能过渡的原因与解决方案
这是一个经典问题:当我们想让元素的高度从 0 变到 auto 时,直接写 transition: height 0.3s 是无效的。
原因:auto 不是一个具体的数值,浏览器无法在过渡开始时计算出中间帧。
方案一:max-height 技巧
.accordion {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.accordion.open {
max-height: 500px; /* 设置为一个比实际内容更大的值 */
}缺点:如果
max-height设置过大,动画会有延迟感。建议设为略高于实际高度的值或使用 JS 动态计算。
方案二:scaleY + transform-origin
.accordion {
transform: scaleY(0);
transform-origin: top center;
transition: transform 0.3s ease;
}
.accordion.open {
transform: scaleY(1);
}方案三:grid-template-rows(现代方案)
.accordion {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s ease;
}
.accordion > .inner {
overflow: hidden;
}
.accordion.open {
grid-template-rows: 1fr;
}这是 2024 年后最推荐的方案,无需预设高度,且动画流畅。
二、CSS 动画(Animation)
CSS 过渡只能处理"从 A 到 B"的单次变化,而 CSS 动画通过 @keyframes 可以定义多阶段、循环或复杂的动画序列。
2.1 @keyframes 定义
@keyframes 规则定义动画的关键帧:
/* 使用百分比定义多阶段 */
@keyframes slideIn {
0% {
transform: translateX(-100%);
opacity: 0;
}
50% {
transform: translateX(10%);
opacity: 0.7;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
/* 只有开始和结束,可用 from/to 简写 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}2.2 动画的八大属性
.element {
/* 逐个定义 */
animation-name: slideIn; /* 动画名称 */
animation-duration: 1s; /* 持续时长 */
animation-timing-function: ease; /* 缓动函数 */
animation-delay: 0s; /* 延迟时间 */
animation-iteration-count: 1; /* 播放次数(infinite 为无限) */
animation-direction: normal; /* 方向 */
animation-fill-mode: none; /* 填充模式 */
animation-play-state: running; /* 播放状态 */
/* 简写形式 */
animation: slideIn 1s ease 0s 1 normal none running;
}| 属性 | 取值 | 说明 |
|---|---|---|
animation-name |
@keyframes 名称 |
引用定义的动画 |
animation-duration |
时间值(如 0.5s) |
必须设置,默认 0s 不播放 |
animation-timing-function |
ease/linear/cubic-bezier() 等 |
同 transition |
animation-delay |
时间值 | 可为负值,负值让动画从中间开始 |
animation-iteration-count |
数字 或 infinite |
0.5 表示只播放前半段 |
animation-direction |
normal / reverse / alternate / alternate-reverse |
动画播放方向 |
animation-fill-mode |
none / forwards / backwards / both |
动画前后样式保持 |
animation-play-state |
running / paused |
控制暂停/播放 |
2.3 animation-direction 详解
@keyframes move {
from { transform: translateX(0); }
to { transform: translateX(200px); }
}
/* 正向:0 → 200px */
.dir-normal {
animation-direction: normal;
}
/* 反向:200px → 0 */
.dir-reverse {
animation-direction: reverse;
}
/* 交替:0→200px→0→200px... */
.dir-alternate {
animation-direction: alternate;
animation-iteration-count: infinite;
}
/* 反向交替:200px→0→200px→0... */
.dir-alternate-reverse {
animation-direction: alternate-reverse;
animation-iteration-count: infinite;
}2.4 animation-fill-mode 详解
fill-mode 控制动画在播放前和播放后如何应用样式:
/* 不保留任何样式(默认) */
.fill-none {
animation: fadeIn 1s;
/* 播放前:元素原始样式
播放后:回到原始样式 */
}
/* 保留最后一帧 */
.fill-forwards {
animation: fadeIn 1s;
animation-fill-mode: forwards;
/* 播放后:保持在 100% 帧的样式 */
}
/* 立即应用到第一帧 */
.fill-backwards {
animation: fadeIn 1s;
animation-delay: 1s;
animation-fill-mode: backwards;
/* 播放前(包括 delay 期间):保持 0% 帧的样式 */
}
/* 同时应用 forwards + backwards */
.fill-both {
animation: fadeIn 1s;
animation-delay: 1s;
animation-fill-mode: both;
}2.5 多动画叠加
一个元素可以同时应用多个动画,用逗号分隔:
.spinner {
animation:
spin 1s linear infinite,
pulse 2s ease-in-out infinite,
colorShift 3s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2); }
}
@keyframes colorShift {
0% { border-color: #e74c3c; }
33% { border-color: #f1c40f; }
66% { border-color: #2ecc71; }
100% { border-color: #e74c3c; }
}2.6 动画事件
CSS 动画会触发三个 JavaScript 事件,方便我们在动画的不同阶段执行逻辑:
const el = document.querySelector('.animated');
el.addEventListener('animationstart', (e) => {
console.log('动画开始', e.animationName);
});
el.addEventListener('animationend', (e) => {
console.log('动画结束', e.animationName);
// 动画结束后移除元素
el.remove();
});
el.addEventListener('animationiteration', (e) => {
console.log('新一轮迭代开始', e.animationName);
});完整示例:
<div class="toast">消息已发送</div>
<script>
const toast = document.querySelector('.toast');
toast.addEventListener('animationend', () => {
toast.style.display = 'none';
});
</script>
<style>
.toast {
animation: slideUp 0.3s ease forwards;
}
@keyframes slideUp {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
</style>2.7 动画性能注意事项
- 优先使用 transform 和 opacity:它们不触发重排(reflow)和重绘(repaint),仅触发合成(composite)。
- 避免动画 width、height、top、left:每次变化都会触发布局计算。
- 适当使用 will-change:为即将动画的元素设置
will-change可提示浏览器提前优化。 - 不要过度使用 infinite 动画:持续运行的动画会增加 GPU 和 CPU 负担。
三、CSS Transform
transform 属性允许我们对元素进行平移、旋转、缩放、倾斜等变换,是 CSS 动画中最核心的搭档。
3.1 2D 变换函数
translate() — 平移
.translate-demo {
/* 单轴 */
transform: translateX(50px); /* X 轴右移 50px */
transform: translateY(-30px); /* Y 轴上移 30px */
/* 双轴 */
transform: translate(50px, -30px);
/* 百分比:相对于自身宽高 */
transform: translate(50%, -50%); /* 水平居中技巧 */
}
/* 经典水平垂直居中 */
.center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}rotate() — 旋转
.rotate-demo {
transform: rotate(45deg); /* 顺时针 45° */
transform: rotate(-90deg); /* 逆时针 90° */
transform: rotate(1turn); /* 一整圈 */
transform: rotate(0.5turn); /* 半圈 */
}scale() — 缩放
.scale-demo {
transform: scale(1.5); /* 等比例放大 1.5 倍 */
transform: scale(0.5); /* 缩小一半 */
transform: scaleX(2); /* X 轴拉伸 2 倍 */
transform: scaleY(0.8); /* Y 轴压缩到 80% */
transform: scale(1.2, 0.8); /* X 和 Y 不同比例 */
}skew() — 倾斜
.skew-demo {
transform: skew(10deg); /* X 轴倾斜 10° */
transform: skew(10deg, 5deg); /* X 轴 10°, Y 轴 5° */
transform: skewX(-15deg); /* X 轴反向倾斜 */
transform: skewY(20deg); /* Y 轴倾斜 */
}matrix() — 矩阵变换
matrix(a, b, c, d, tx, ty) 是 2D 变换的通用矩阵形式,包含缩放、旋转、倾斜和平移。通常不用手写,但了解其原理有助于深入理解变换本质:
/* matrix(scaleX, skewY, skewX, scaleY, translateX, translateY) */
/* translate(50px, 30px) 等价于 */
transform: matrix(1, 0, 0, 1, 50, 30);
/* rotate(45deg) 等价于 */
transform: matrix(0.7071, 0.7071, -0.7071, 0.7071, 0, 0);
/* scale(2, 1.5) 等价于 */
transform: matrix(2, 0, 0, 1.5, 0, 0);3.2 transform-origin
transform-origin 设置变换的原点(默认是元素中心 50% 50%):
.origin-demo {
transform: rotate(45deg);
/* 关键字 */
transform-origin: top left; /* 左上角 */
transform-origin: center; /* 中心(默认) */
transform-origin: right bottom; /* 右下角 */
/* 数值 */
transform-origin: 0 0; /* 左上角 */
transform-origin: 100px 200px; /* 指定坐标 */
transform-origin: 50% 100%; /* 底部中心 */
}示例:绕左下角旋转的钟摆效果
.pendulum {
transform-origin: top center;
animation: swing 1s ease-in-out infinite alternate;
}
@keyframes swing {
from { transform: rotate(-30deg); }
to { transform: rotate(30deg); }
}3.3 3D Transform
CSS 3D 变换让元素在三维空间中变换,为页面带来真正的立体感。
基础 3D 函数
.three-d-demo {
/* 3D 平移 */
transform: translateZ(100px); /* 向用户方向移动 */
transform: translate3d(x, y, z); /* 3D 平移简写 */
/* 3D 旋转 */
transform: rotateX(45deg); /* 绕 X 轴旋转(上下翻转) */
transform: rotateY(45deg); /* 绕 Y 轴旋转(左右翻转) */
transform: rotateZ(45deg); /* 绕 Z 轴旋转(平面旋转,同 rotate()) */
transform: rotate3d(x, y, z, deg); /* 绕任意轴旋转 */
/* 3D 缩放 */
transform: scaleZ(1.5); /* Z 轴缩放(影响厚度) */
transform: scale3d(x, y, z); /* 3D 缩放简写 */
}perspective — 透视
透视是 3D 效果的关键,它决定了观察者到物体的距离:
/* 方式一:在父容器上设置透视 */
.scene {
perspective: 800px; /* 观察距离 800px */
perspective-origin: 50% 50%; /* 观察点位置 */
}
.scene .card {
transform: rotateY(45deg);
}
/* 方式二:在 transform 中直接设置 */
.card {
transform: perspective(800px) rotateY(45deg);
}两种方式的区别:
- 父容器设置
perspective:所有子元素共享同一个透视空间 transform: perspective():每个元素独立计算透视
transform-style: preserve-3d
默认情况下,子元素的 3D 变换会被"拍平"到父元素所在的平面。preserve-3d 保留子元素的 3D 位置:
.scene {
perspective: 800px;
transform-style: preserve-3d; /* 保留子元素 3D 位置 */
}
.card-front,
.card-back {
backface-visibility: hidden; /* 隐藏背面 */
}
.card-back {
transform: rotateY(180deg); /* 背面旋转 180° */
}backface-visibility
控制元素背面是否可见,在卡片翻转效果中至关重要:
.card-face {
backface-visibility: hidden; /* 背面不可见 */
/* 或 */
backface-visibility: visible; /* 背面可见(默认) */
}3.4 3D 卡片翻转效果
这是一个完整的 3D 翻转卡片案例:
<div class="card-container">
<div class="card">
<div class="card-face card-front">
<h2>正面</h2>
<p>悬停或点击翻转</p>
</div>
<div class="card-face card-back">
<h2>背面</h2>
<p>更多内容在这里</p>
</div>
</div>
</div>
<style>
.card-container {
width: 300px;
height: 200px;
perspective: 1000px;
}
.card {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
}
.card:hover {
transform: rotateY(180deg);
}
.card-face {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 12px;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
.card-front {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.card-back {
background: linear-gradient(135deg, #f093fb, #f5576c);
color: white;
transform: rotateY(180deg);
}
</style>3.5 性能提示:transform + opacity 走合成层
浏览器渲染流程可分为以下步骤:
JavaScript → Style → Layout → Paint → Composite- Layout(重排):计算元素的位置和大小。改变
width、height、top、left等会触发。 - Paint(重绘):绘制像素。改变
color、background-color、box-shadow等会触发。 - Composite(合成):将图层合成到屏幕上。改变
transform和opacity仅触发合成。
核心原则:尽可能只使用 transform 和 opacity 做动画,避免触发 Layout 和 Paint。
/* ❌ 不好:触发重排 */
.bad {
animation: badMove 1s ease infinite;
}
@keyframes badMove {
from { left: 0; }
to { left: 100px; }
}
/* ✅ 好:仅触复合层 */
.good {
animation: goodMove 1s ease infinite;
}
@keyframes goodMove {
from { transform: translateX(0); }
to { transform: translateX(100px); }
}四、will-change 与性能优化
4.1 哪些属性触发重排/重绘/合成
| 触发类别 | 属性举例 | 性能影响 |
|---|---|---|
| 仅合成 | transform、opacity |
✅ 最优 |
| 重绘+合成 | color、background-color、box-shadow、border-radius |
⚠️ 中等 |
| 重排+重绘+合成 | width、height、top、left、margin、padding、font-size、border-width |
❌ 最差 |
4.2 will-change 的使用时机
will-change 属性告诉浏览器某个元素即将发生变化,让浏览器提前做好优化准备:
/* 告诉浏览器 opacity 将发生变化 */
.element {
will-change: opacity;
}
/* 多个属性 */
.element {
will-change: transform, opacity;
}
/* 告诉浏览器将创建新的层叠上下文 */
.element {
will-change: transform;
}使用原则:
- 不要对大量元素使用:每个
will-change都会创建新的合成层,过多会耗尽 GPU 内存。 - 在变化前添加,变化后移除:这是理想做法,通常用 JS 控制:
const el = document.querySelector('.animated');
// 动画开始前添加
el.addEventListener('mouseenter', () => {
el.style.willChange = 'transform';
});
// 动画结束后移除
el.addEventListener('transitionend', () => {
el.style.willChange = 'auto';
});- 不要对无动画的元素使用:
will-change本身有性能开销。
4.3 强制 GPU 合成
在少数情况下,浏览器可能没有将元素提升到合成层,我们可以通过一些技巧强制触发:
/* 方法一:使用 will-change */
.gpu-layer {
will-change: transform;
}
/* 方法二:使用 3D transform 强制提升 */
.gpu-layer {
transform: translateZ(0); /* 或 translate3d(0,0,0) */
}
/* 方法三:使用 backface-visibility */
.gpu-layer {
backface-visibility: hidden;
}注意:过度使用 GPU 合成会占用显存,尤其在有大量动画元素时可能适得其反。只在必要的时候使用。
五、实战效果
5.1 加载动画
旋转加载器
<div class="spinner"></div>
<style>
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e0e0e0;
border-top-color: #3498db;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>进度条动画
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
<style>
.progress-bar {
width: 300px;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #3498db, #2ecc71);
border-radius: 4px;
animation: fillProgress 2s ease-out forwards;
}
@keyframes fillProgress {
from { width: 0%; }
to { width: 80%; }
}
</style>骨架屏
<div class="skeleton-card">
<div class="skeleton-avatar skeleton-pulse"></div>
<div class="skeleton-lines">
<div class="skeleton-line skeleton-pulse"></div>
<div class="skeleton-line skeleton-pulse short"></div>
</div>
</div>
<style>
.skeleton-card {
display: flex;
gap: 12px;
padding: 16px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.skeleton-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: #e0e0e0;
}
.skeleton-lines {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 8px;
}
.skeleton-line {
height: 12px;
background: #e0e0e0;
border-radius: 6px;
}
.skeleton-line.short {
width: 60%;
}
.skeleton-pulse {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
</style>5.2 悬浮效果
缩放 + 阴影
<div class="hover-card">
<img src="https://picsum.photos/300/200" alt="示例图片">
<h3>卡片标题</h3>
</div>
<style>
.hover-card {
width: 300px;
border-radius: 12px;
overflow: hidden;
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition:
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
}
.hover-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 12px 24px rgba(0,0,0,0.15);
}
.hover-card img {
width: 100%;
transition: transform 0.3s ease;
}
.hover-card:hover img {
transform: scale(1.1);
}
.hover-card h3 {
padding: 16px;
margin: 0;
}
</style>边框动画
<button class="border-btn">悬停我</button>
<style>
.border-btn {
padding: 12px 32px;
font-size: 16px;
color: #3498db;
background: transparent;
border: 2px solid #3498db;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: color 0.3s ease;
z-index: 1;
}
.border-btn::before {
content: '';
position: absolute;
inset: 0;
background: #3498db;
transform: scaleX(0);
transform-origin: right center;
transition: transform 0.3s ease;
z-index: -1;
}
.border-btn:hover {
color: #fff;
}
.border-btn:hover::before {
transform: scaleX(1);
transform-origin: left center;
}
</style>5.3 页面进入动画(Intersection Observer + 样式)
利用 Intersection Observer API 监听元素进入视口,再通过 class 切换触发动画:
<div class="scroll-container">
<div class="scroll-item fade-in-up" data-delay="0">项目 1</div>
<div class="scroll-item fade-in-up" data-delay="100">项目 2</div>
<div class="scroll-item fade-in-up" data-delay="200">项目 3</div>
<div class="scroll-item fade-in-up" data-delay="300">项目 4</div>
</div>
<style>
.scroll-item {
height: 200px;
margin: 20px 0;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
font-size: 24px;
opacity: 0;
transform: translateY(30px);
transition:
opacity 0.6s ease,
transform 0.6s ease;
}
.scroll-item.visible {
opacity: 1;
transform: translateY(0);
}
</style>
<script>
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// 支持延迟
const delay = entry.target.dataset.delay || 0;
setTimeout(() => {
entry.target.classList.add('visible');
}, delay);
// 只触发一次
observer.unobserve(entry.target);
}
});
}, {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
});
document.querySelectorAll('.scroll-item').forEach((el) => {
observer.observe(el);
});
</script>5.4 3D 翻转卡片(完整版)
<div class="scene">
<div class="flip-card">
<div class="flip-card-inner">
<div class="flip-card-front">
<img src="https://picsum.photos/300/400?random=1" alt="正面">
</div>
<div class="flip-card-back">
<h3>背面信息</h3>
<p>详细的描述文字...</p>
<button>了解更多</button>
</div>
</div>
</div>
</div>
<style>
.scene {
width: 300px;
height: 400px;
perspective: 1200px;
}
.flip-card {
width: 100%;
height: 100%;
cursor: pointer;
}
.flip-card-inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
.flip-card:hover .flip-card-inner {
transform: rotateY(180deg);
}
.flip-card-front,
.flip-card-back {
position: absolute;
inset: 0;
border-radius: 16px;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
overflow: hidden;
}
.flip-card-front img {
width: 100%;
height: 100%;
object-fit: cover;
}
.flip-card-back {
background: linear-gradient(135deg, #2c3e50, #3498db);
color: white;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
text-align: center;
transform: rotateY(180deg);
}
.flip-card-back button {
margin-top: 16px;
padding: 10px 24px;
background: white;
color: #3498db;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
transition: transform 0.2s ease;
}
.flip-card-back button:hover {
transform: scale(1.05);
}
</style>5.5 无缝轮播
<div class="carousel">
<div class="carousel-track">
<div class="carousel-slide">1</div>
<div class="carousel-slide">2</div>
<div class="carousel-slide">3</div>
<div class="carousel-slide">4</div>
<div class="carousel-slide">5</div>
<!-- 复制第一张用于无缝衔接 -->
<div class="carousel-slide">1</div>
</div>
</div>
<style>
.carousel {
width: 400px;
overflow: hidden;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.carousel-track {
display: flex;
width: 600%; /* 6 张幻灯片 */
animation: carouselScroll 12s linear infinite;
}
.carousel-slide {
width: 400px;
height: 250px;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
color: white;
font-weight: bold;
flex-shrink: 0;
}
.carousel-slide:nth-child(1) { background: #e74c3c; }
.carousel-slide:nth-child(2) { background: #3498db; }
.carousel-slide:nth-child(3) { background: #2ecc71; }
.carousel-slide:nth-child(4) { background: #f39c12; }
.carousel-slide:nth-child(5) { background: #9b59b6; }
.carousel-slide:nth-child(6) { background: #e74c3c; }
@keyframes carouselScroll {
0% { transform: translateX(0); }
16.666% { transform: translateX(-400px); } /* 1→2 */
33.333% { transform: translateX(-800px); } /* 2→3 */
50% { transform: translateX(-1200px); } /* 3→4 */
66.666% { transform: translateX(-1600px); } /* 4→5 */
83.333% { transform: translateX(-2000px); } /* 5→1(副本) */
100% { transform: translateX(-2000px); } /* 停顿,然后瞬间复位 */
}
</style>
<script>
// JS 控制复位(无感跳回第一张)
const track = document.querySelector('.carousel-track');
track.addEventListener('animationiteration', () => {
// 此时动画在最后一帧,将 track 复位到初始位置
// 用 JS 更精细控制实际项目中更常见
});
</script>实际生产环境中的无缝轮播通常用 JS +
transform配合transition实现更精细的控制,这里展示的是纯 CSS 方案的工作原理。
5.6 呼吸灯效果
<div class="breathing-light"></div>
<style>
.breathing-light {
width: 100px;
height: 100px;
background: radial-gradient(circle, #ff6b6b, #ee5a24);
border-radius: 50%;
box-shadow:
0 0 20px rgba(255, 107, 107, 0.6),
0 0 40px rgba(255, 107, 107, 0.4),
0 0 80px rgba(255, 107, 107, 0.2);
animation: breathe 2s ease-in-out infinite;
}
@keyframes breathe {
0%, 100% {
transform: scale(0.8);
box-shadow:
0 0 20px rgba(255, 107, 107, 0.4),
0 0 40px rgba(255, 107, 107, 0.2);
}
50% {
transform: scale(1.2);
box-shadow:
0 0 30px rgba(255, 107, 107, 0.8),
0 0 60px rgba(255, 107, 107, 0.5),
0 0 100px rgba(255, 107, 107, 0.3);
}
}
</style>5.7 逐帧动画(steps + sprite)
逐帧动画使用 steps() 配合精灵图(sprite sheet)实现类似 GIF 的动画效果:
<div class="sprite-animation"></div>
<style>
.sprite-animation {
width: 100px; /* 一帧的宽度 */
height: 100px; /* 一帧的高度 */
background: url('https://via.placeholder.com/600x100/3498db/ffffff?text=012345') no-repeat;
/* 假设精灵图:6 帧水平排列,总宽 600px */
background-size: 600px 100px;
animation: spritePlay 0.6s steps(5, end) infinite;
}
@keyframes spritePlay {
from {
background-position: 0 0; /* 第一帧 */
}
to {
background-position: -600px 0; /* 最后一帧(超出,被 steps 截断) */
}
}
</style>关键原理:
- 精灵图宽 600px,每帧 100px,共 6 帧
steps(5, end)将动画分为 5 步(从第 0 帧到第 5 帧,共 6 帧)background-position从0移动到-600px,每次跳跃 100px
实际工作中的精灵图示例(以奔跑的小人为例):
.runner {
width: 64px;
height: 64px;
background: url('run-sprite.png') no-repeat;
/* 假设 8 帧横向排列,总宽 512px */
background-size: 512px 64px;
animation: run 0.8s steps(7, end) infinite;
}
@keyframes run {
from { background-position: 0 0; }
to { background-position: -512px 0; }
}六、CSS vs JS 动画
6.1 对比概览
| 维度 | CSS 动画 | JS 动画(requestAnimationFrame) |
|---|---|---|
| 易用性 | ⭐⭐⭐⭐⭐ 声明式,几行代码 | ⭐⭐⭐ 需要手动控制 |
| 性能 | ⭐⭐⭐⭐⭐ 浏览器优化,走合成层 | ⭐⭐⭐⭐ 需注意优化 |
| 控制力 | ⭐⭐⭐ 有限(暂停/反转/时间线) | ⭐⭐⭐⭐⭐ 完全控制 |
| 复杂序列 | ⭐⭐ 需要多 keyframes 或延迟 | ⭐⭐⭐⭐⭐ 任意逻辑 |
| 交互绑定 | ⭐⭐ 需借助 class/css 变量 | ⭐⭐⭐⭐⭐ 可响应拖拽、滚动等 |
| 兼容性 | ⭐⭐⭐⭐ 现代浏览器支持好 | ⭐⭐⭐⭐⭐ 全平台一致 |
6.2 何时用 CSS
- 简单的一次性过渡:按钮悬浮、表单聚焦、菜单展开/收起
- 持续的 UI 微交互:加载旋转、脉冲效果、呼吸灯
- 进入/退出动画:配合
visibility或 class 切换 - 页面加载动画:从初始状态到最终状态
6.3 何时用 JS
- 复杂的时间线编排:多个动画按精确时间轴执行
- 动态计算:动画路径、时长、参数由用户交互或数据决定
- 物理模拟:弹簧效果、贝塞尔模拟、粒子系统
- 滚动驱动动画:视差滚动、滚动进度条
- 暂停/倒放/跳转:视频播放器式的动画控制
6.4 requestAnimationFrame 示例
function animate(element, targetX, duration = 1000) {
const startX = parseFloat(getComputedStyle(element).left) || 0;
const startTime = performance.now();
function frame(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1); // 0 → 1
// easeOut 缓动
const eased = 1 - Math.pow(1 - progress, 3);
const currentX = startX + (targetX - startX) * eased;
element.style.transform = `translateX(${currentX}px)`;
if (progress < 1) {
requestAnimationFrame(frame);
}
}
requestAnimationFrame(frame);
}
// 使用
const box = document.querySelector('.box');
animate(box, 300, 1500);6.5 Web Animations API 简介
Web Animations API 是浏览器原生提供的 JS 动画接口,结合了 CSS 动画的声明式简洁和 JS 的过程式控制:
const element = document.querySelector('.animated');
const animation = element.animate(
[
{ transform: 'translateX(0)', opacity: 1 },
{ transform: 'translateX(300px)', opacity: 0.5 },
{ transform: 'translateX(0)', opacity: 1 },
],
{
duration: 2000,
easing: 'ease-in-out',
iterations: Infinity,
direction: 'alternate',
}
);
// 控制方法
animation.pause(); // 暂停
animation.play(); // 播放
animation.reverse(); // 倒放
animation.finish(); // 跳转到结束
animation.cancel(); // 取消
// 读取状态
console.log(animation.playState); // 'running' | 'paused' | 'finished'
console.log(animation.currentTime); // 当前时间位置
animation.currentTime = 500; // 跳转到 500ms 位置
// 事件监听
animation.onfinish = () => {
console.log('动画完成');
};Web Animations API vs CSS 动画:
/* CSS 方式 */
.css-box {
animation: slide 2s ease infinite alternate;
}
@keyframes slide {
from { transform: translateX(0); }
to { transform: translateX(300px); }
}// WAAPI 方式(完全等价)
const box = document.querySelector('.waapi-box');
box.animate([
{ transform: 'translateX(0)' },
{ transform: 'translateX(300px)' }
], {
duration: 2000,
easing: 'ease',
iterations: Infinity,
direction: 'alternate'
});Web Animations API 的优势:
- 可以动态创建和修改动画参数
- 提供完整的播放控制(暂停、倒放、跳转)
- 支持动画组合和时序编排
- 性能等同 CSS 动画(底层相同)
七、总结与最佳实践
核心原则回顾
- 优先用 transform 和 opacity 做动画:它们仅触发合成层,性能最优。
- CSS 过渡用于简单状态变化:hover、focus、class 切换等单次变化。
- CSS 动画用于复杂多阶段效果:循环、多关键帧、需要方向控制。
- JS 动画用于需要动态控制的高阶场景:物理模拟、滚动驱动、时间线编排。
- 合理使用 will-change:为即将动画的元素提前通知浏览器,但用完即弃。
- 用 Web Animations API 替代手写 rAF:在需要 JS 控制时优先选择原生 API。
性能检查清单
- [ ] 动画属性是否只用
transform和opacity? - [ ] 是否避免了
width、height、top、left的动画? - [ ] 是否对长时间运行的动画设置了
will-change? - [ ] 动画元素数量是否可控(建议不超过几十个)?
- [ ] 移动端是否测试了动画流畅度?
- [ ]
animation-iteration-count: infinite是否必要?
常用动画速查表
| 场景 | 推荐方案 | 关键属性 |
|---|---|---|
| 按钮悬浮 | CSS Transition | transform: scale() + box-shadow |
| 弹窗出现 | CSS Animation + JS 控制 | @keyframes + animation-fill-mode: both |
| 列表入场 | Intersection Observer + CSS Transition | 动态添加 class |
| 加载旋转 | CSS Animation | rotate() + infinite |
| 轮播滚动 | JS + CSS Transition / WAAPI | 动态 transform: translateX() |
| 卡片翻转 | CSS 3D Transition | rotateY() + preserve-3d + backface-visibility |
| 逐帧动画 | CSS Animation + steps() |
background-position + steps() |
| 物理弹簧 | JS + rAF 或 WAAPI | 自定义缓动函数 |