CSS 动画
CSS 动画让我们可以在不使用 JavaScript 的情况下创建流畅的动画效果。本章将详细介绍 CSS 动画的完整用法。
动画基础概念
什么是 CSS 动画?
CSS 动画让元素从一个样式逐渐变化到另一个样式。与 CSS 过渡(transition)相比,动画有以下优势:
- 可以创建多步骤的动画序列
- 可以循环播放
- 更精细的控制(暂停、反向播放等)
- 不需要触发条件(如 hover)
动画的两个部分
创建 CSS 动画需要两部分配合:
- @keyframes 规则:定义动画的关键帧和样式变化
- animation 属性:将动画应用到元素上
/* 第一部分:定义关键帧 */
@keyframes slideIn {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
/* 第二部分:应用动画 */
.element {
animation: slideIn 0.5s ease-out;
}
@keyframes 关键帧
基本语法
@keyframes 规则定义动画的中间步骤:
@keyframes 动画名称 {
关键帧选择器 {
样式属性: 值;
}
}
from 和 to
使用 from 和 to 定义动画的起点和终点:
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 等价于使用百分比 */
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
多个关键帧
使用百分比定义更复杂的动画序列:
@keyframes bounce {
0% {
transform: translateY(0);
}
25% {
transform: translateY(-30px);
}
50% {
transform: translateY(0);
}
75% {
transform: translateY(-15px);
}
100% {
transform: translateY(0);
}
}
关键帧的要点
关键帧顺序无关紧要
浏览器会按百分比顺序执行,书写顺序不影响动画:
/* 这两种写法效果相同 */
@keyframes example1 {
100% { color: red; }
0% { color: blue; }
}
@keyframes example2 {
0% { color: blue; }
100% { color: red; }
}
省略起始帧
如果省略 0% 或 from,浏览器使用元素的初始样式:
/* 元素会从其原始位置开始动画 */
@keyframes moveRight {
to {
transform: translateX(100px);
}
}
多个属性同时动画
@keyframes multiProperty {
0% {
opacity: 0;
transform: scale(0.5);
}
50% {
opacity: 1;
transform: scale(1.1);
}
100% {
opacity: 1;
transform: scale(1);
}
}
重复关键帧的处理
同一百分比可以出现多次,样式会合并:
@keyframes example {
50% {
top: 10px;
}
50% {
left: 20px;
}
/* 50% 时的样式是 top: 10px 和 left: 20px */
}
animation 属性详解
animation 是一个简写属性,包含以下子属性:
| 属性 | 说明 | 默认值 |
|---|---|---|
| animation-name | 动画名称 | none |
| animation-duration | 动画时长 | 0s |
| animation-timing-function | 时间函数 | ease |
| animation-delay | 延迟时间 | 0s |
| animation-iteration-count | 播放次数 | 1 |
| animation-direction | 播放方向 | normal |
| animation-fill-mode | 填充模式 | none |
| animation-play-state | 播放状态 | running |
animation-name 动画名称
指定要使用的 @keyframes 动画:
.element {
animation-name: slideIn;
}
/* 不应用动画 */
.element {
animation-name: none;
}
animation-duration 动画时长
指定动画完成一个周期的时间:
.element {
animation-duration: 2s; /* 2秒 */
animation-duration: 500ms; /* 500毫秒 */
animation-duration: 0.5s; /* 0.5秒 */
}
如果时长为 0s(默认值),动画不会显示,但动画事件仍会触发。
animation-timing-function 时间函数
时间函数控制动画的速度曲线,决定了动画在每个时间点的进度。理解时间函数是创建自然流畅动画的关键。
预设关键字
/* 关键字 */
animation-timing-function: ease; /* 慢-快-慢(默认) */
animation-timing-function: linear; /* 匀速 */
animation-timing-function: ease-in; /* 慢开始 */
animation-timing-function: ease-out; /* 慢结束 */
animation-timing-function: ease-in-out; /* 慢开始和结束 */
常见时间函数对比:
| 函数 | 效果 | 适用场景 |
|---|---|---|
| ease | 开始慢,中间快,结束慢 | 通用动画,自然过渡 |
| linear | 匀速 | 进度条、旋转、颜色循环 |
| ease-in | 开始慢,逐渐加速 | 元素离开屏幕、关闭效果 |
| ease-out | 开始快,逐渐减速 | 元素进入屏幕、弹出效果 |
| ease-in-out | 慢速开始和结束 | 需要平滑过渡的动画 |
自定义贝塞尔曲线
cubic-bezier() 函数允许创建自定义的速度曲线:
/* 语法:cubic-bezier(x1, y1, x2, y2) */
animation-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
参数说明:
四个参数定义了两个控制点 (x1, y1) 和 (x2, y2),它们控制贝塞尔曲线的形状:
- x 值必须在 0 到 1 之间(表示时间进度)
- y 值可以超出 0-1 范围(表示动画进度的"弹性")
常用自定义曲线:
/* 弹性效果 */
.bounce {
animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
/* 快速减速 */
.snap {
animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
/* 平滑开始,快速结束 */
.smooth-start {
animation-timing-function: cubic-bezier(0.0, 0.0, 0.2, 1);
}
/* 快速开始,平滑结束 */
.smooth-end {
animation-timing-function: cubic-bezier(0.4, 0.0, 0.6, 1);
}
ease 等价值:
/* ease 等价于 */
animation-timing-function: ease;
animation-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
steps() 步进函数
steps() 函数将动画分成离散的步骤,而不是平滑过渡:
/* 语法:steps(步数, 跳跃时机) */
animation-timing-function: steps(4, end);
animation-timing-function: step-start; /* 等于 steps(1, start) */
animation-timing-function: step-end; /* 等于 steps(1, end) */
跳跃时机说明:
| 值 | 说明 |
|---|---|
start | 每步开始时立即跳到该步的结束值 |
end | 每步结束时才跳到下一步的值(默认) |
应用示例:
/* 精灵图动画 */
@keyframes walk {
from { background-position: 0 0; }
to { background-position: -192px 0; } /* 6帧,每帧32px */
}
.character {
width: 32px;
height: 32px;
background: url('sprite.png');
animation: walk 0.5s steps(6) infinite;
}
/* 打字机效果 */
@keyframes typing {
from { width: 0; }
to { width: 100%; }
}
.typewriter {
overflow: hidden;
white-space: nowrap;
animation: typing 3s steps(30) forwards;
/* 30 表示字符数 */
}
/* 数字翻牌效果 */
@keyframes flip {
from { transform: translateY(0); }
to { transform: translateY(-100%); }
}
.digit {
animation: flip 1s steps(10) infinite;
}
关键帧中使用不同的时间函数
每个关键帧可以设置不同的时间函数,实现复杂的动画效果:
@keyframes complex-bounce {
0% {
transform: translateY(0);
animation-timing-function: ease-out; /* 下落时加速 */
}
50% {
transform: translateY(100px);
animation-timing-function: ease-in; /* 弹起时减速 */
}
100% {
transform: translateY(0);
}
}
注意: 关键帧中的 animation-timing-function 只影响到下一个关键帧的过渡。
animation-delay 延迟时间
指定动画开始前的等待时间:
.element {
animation-delay: 1s; /* 延迟1秒开始 */
animation-delay: -1s; /* 负值:从动画第1秒开始播放 */
}
负延迟的用途:
/* 让动画从中间开始播放 */
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.element {
animation: spin 4s linear infinite;
animation-delay: -2s; /* 从旋转到一半的位置开始 */
}
animation-iteration-count 播放次数
animation-iteration-count: 1; /* 播放1次(默认) */
animation-iteration-count: 3; /* 播放3次 */
animation-iteration-count: infinite; /* 无限循环 */
animation-iteration-count: 2.5; /* 播放2.5次 */
animation-direction 播放方向
animation-direction: normal; /* 正向播放(默认) */
animation-direction: reverse; /* 反向播放 */
animation-direction: alternate; /* 正向后反向,交替播放 */
animation-direction: alternate-reverse; /* 反向后正向,交替播放 */
alternate 的典型用法:
/* 元素来回移动 */
.element {
animation: moveRight 2s ease-in-out infinite alternate;
}
@keyframes moveRight {
from { transform: translateX(0); }
to { transform: translateX(200px); }
}
/* 播放顺序:0→200px→0→200px... */
animation-fill-mode 填充模式
控制动画执行前后的样式状态:
animation-fill-mode: none; /* 默认:动画前后应用元素原始样式 */
animation-fill-mode: forwards; /* 动画结束后保持最后一帧样式 */
animation-fill-mode: backwards; /* 动画开始前应用第一帧样式 */
animation-fill-mode: both; /* 同时应用 forwards 和 backwards */
详细说明:
/* forwards:动画结束后保持在终点位置 */
.fade-in {
animation: fadeIn 1s forwards;
/* 动画结束后 opacity 保持为 1 */
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* backwards:有延迟时,在等待期间显示第一帧样式 */
.element {
animation: slideIn 1s 2s backwards;
/* 在2秒等待期间就显示 translateX(-100%) 的位置 */
}
@keyframes slideIn {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
/* both:结合两种效果 */
.element {
animation: slideIn 1s 2s both;
/* 等待期间显示起点,结束后保持终点 */
}
animation-play-state 播放状态
animation-play-state: running; /* 播放中(默认) */
animation-play-state: paused; /* 暂停 */
悬停暂停动画:
.element {
animation: spin 4s linear infinite;
}
.element:hover {
animation-play-state: paused;
}
animation 简写属性
/* 完整语法 */
animation: name duration timing-function delay iteration-count direction fill-mode play-state;
/* 常用示例 */
animation: slideIn 0.5s ease-out;
animation: fadeIn 1s 0.5s forwards;
animation: bounce 2s ease-in-out infinite alternate;
/* 多个动画 */
animation:
fadeIn 1s,
slideIn 1s 0.5s;
- 时间值有两个时,第一个是 duration,第二个是 delay
- animation-name 建议放在最后,避免被其他值解析错误
实用动画示例
1. 淡入淡出
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.fade-in {
animation: fadeIn 0.5s ease-out forwards;
}
/* 淡出需要配合 JavaScript 移除元素 */
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.fade-out {
animation: fadeOut 0.5s ease-out forwards;
}
2. 滑入效果
/* 从左侧滑入 */
@keyframes slideInLeft {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.slide-in-left {
animation: slideInLeft 0.5s ease-out forwards;
}
/* 从右侧滑入 */
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.slide-in-right {
animation: slideInRight 0.5s ease-out forwards;
}
3. 弹跳效果
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-30px);
}
60% {
transform: translateY(-15px);
}
}
.bounce {
animation: bounce 1s ease infinite;
}
4. 旋转加载
@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;
}
5. 脉冲效果
@keyframes pulse {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(52, 152, 219, 0.7);
}
70% {
transform: scale(1.1);
box-shadow: 0 0 0 10px rgba(52, 152, 219, 0);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(52, 152, 219, 0);
}
}
.pulse {
animation: pulse 2s infinite;
}
6. 摇晃效果
@keyframes shake {
0%, 100% {
transform: translateX(0);
}
10%, 30%, 50%, 70%, 90% {
transform: translateX(-10px);
}
20%, 40%, 60%, 80% {
transform: translateX(10px);
}
}
.shake {
animation: shake 0.5s ease-in-out;
}
7. 打字机效果
@keyframes typing {
from {
width: 0;
}
to {
width: 100%;
}
}
@keyframes blink {
50% {
border-color: transparent;
}
}
.typewriter {
overflow: hidden;
white-space: nowrap;
border-right: 2px solid;
animation:
typing 3s steps(30) forwards,
blink 0.5s step-end infinite;
}
8. 渐变背景动画
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.animated-gradient {
background: linear-gradient(270deg, #3498db, #e74c3c, #2ecc71);
background-size: 600% 600%;
animation: gradient 5s ease infinite;
}
动画与性能
可动画属性
并非所有 CSS 属性都支持动画。以下是常见的可动画属性:
变换属性(推荐使用):
- transform(translate、scale、rotate、skew)
- opacity
盒模型属性:
- width、height
- margin、padding
- top、right、bottom、left
文本属性:
- color、font-size
- letter-spacing、word-spacing
- line-height
背景属性:
- background-color
- background-position
性能优化建议
优先使用 transform 和 opacity
/* 推荐:使用 transform */
@keyframes move {
to {
transform: translateX(100px);
}
}
/* 避免:直接改变位置属性 */
@keyframes moveBad {
to {
left: 100px;
}
}
使用 will-change 提示浏览器
.animated-element {
will-change: transform, opacity;
}
只在需要时使用,过度使用会消耗更多资源。
避免动画盒模型属性
/* 不推荐:动画 width 会触发布局重排 */
@keyframes expand {
to { width: 200px; }
}
/* 推荐:使用 transform: scaleX() */
@keyframes expand {
to { transform: scaleX(2); }
}
滚动驱动动画
滚动驱动动画(Scroll-driven Animations)是 CSS 2024 年新增的重要特性,它允许动画随滚动进度播放,而不是随时间播放。这意味着用户可以通过滚动来控制动画的进度,创建更自然的交互体验。
什么是滚动驱动动画?
传统 CSS 动画基于时间(animation-duration),而滚动驱动动画基于滚动位置。动画的进度直接与滚动位置关联:滚动到顶部时动画在 0%,滚动到底部时动画在 100%。
优势:
- 无需 JavaScript 即可实现滚动动画
- 性能更好,不占用主线程
- 进度完全由用户控制,更自然
animation-timeline 属性
animation-timeline 属性用于指定动画使用的时间线:
/* 默认:基于时间的时间线 */
animation-timeline: auto;
/* 无时间线(不执行动画) */
animation-timeline: none;
/* 滚动进度时间线 */
animation-timeline: --my-scroll-timeline;
animation-timeline: scroll();
animation-timeline: view();
animation 简写属性会将 animation-timeline 重置为 auto。因此,声明滚动驱动动画时,animation-timeline 必须放在 animation 之后。
滚动进度时间线 Scroll Progress Timeline
滚动进度时间线根据滚动容器的滚动位置来推进动画。
使用 scroll() 函数(匿名时间线)
最简单的方式是使用 scroll() 函数:
@keyframes fade-in {
from { opacity: 0; transform: translateY(50px); }
to { opacity: 1; transform: translateY(0); }
}
.element {
animation: fade-in linear;
animation-timeline: scroll();
}
scroll() 函数参数:
/* 语法:scroll(<scroller>, <axis>) */
/* 使用最近的滚动容器,垂直方向 */
animation-timeline: scroll();
/* 使用根滚动容器(页面滚动) */
animation-timeline: scroll(root);
/* 使用最近的滚动容器 */
animation-timeline: scroll(nearest);
/* 元素自身滚动 */
animation-timeline: scroll(self);
/* 指定滚动方向 */
animation-timeline: scroll(block); /* 块方向(默认,垂直) */
animation-timeline: scroll(inline); /* 行内方向(水平) */
animation-timeline: scroll(y); /* Y轴 */
animation-timeline: scroll(x); /* X轴 */
/* 组合使用 */
animation-timeline: scroll(root block);
示例:阅读进度指示器
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: #3498db;
transform-origin: left;
animation: scale-x linear;
animation-timeline: scroll(root);
}
@keyframes scale-x {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
命名滚动时间线
使用 scroll-timeline-name 创建命名时间线,实现更精确的控制:
/* 滚动容器 */
.scroll-container {
overflow-y: auto;
scroll-timeline-name: --my-timeline;
/* 或使用简写 */
scroll-timeline: --my-timeline y;
}
/* 动画元素 */
.animated-element {
animation: my-animation linear;
animation-timeline: --my-timeline;
}
@keyframes my-animation {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
scroll-timeline 相关属性:
| 属性 | 说明 |
|---|---|
scroll-timeline-name | 时间线名称 |
scroll-timeline-axis | 滚动轴(block/inline/x/y) |
scroll-timeline | 简写属性 |
视图进度时间线 View Progress Timeline
视图进度时间线根据元素在视口中的可见性来推进动画。当元素进入视口时动画开始,离开视口时动画结束。
使用 view() 函数
/* 基本用法 */
.card {
animation: reveal linear;
animation-timeline: view();
}
@keyframes reveal {
from {
opacity: 0;
transform: translateY(100px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
view() 函数参数:
/* 语法:view(<axis>, <inset>) */
/* 默认:块方向,无偏移 */
animation-timeline: view();
/* 指定轴 */
animation-timeline: view(block);
animation-timeline: view(inline);
animation-timeline: view(x);
animation-timeline: view(y);
/* 指定偏移量 */
animation-timeline: view(block 50%); /* 元素进入视口 50% 后开始 */
animation-timeline: view(block 0 200px); /* 开始偏移 0,结束偏移 200px */
animation-timeline: view(block 20% 20%); /* 两侧各偏移 20% */
调整动画范围
由于视图时间线在元素完全离开视口时才达到 100%,通常需要提前完成动画:
/* 推荐做法:在元素还在视口内时完成动画 */
@keyframes slide-in {
0% {
opacity: 0;
transform: translateX(-100px);
}
20% {
opacity: 1;
transform: translateX(0);
}
80% {
opacity: 1;
transform: translateX(0);
}
100% {
opacity: 0;
transform: translateX(100px);
}
}
命名视图时间线
/* 被追踪的元素 */
.subject {
view-timeline-name: --subject-timeline;
/* 或简写 */
view-timeline: --subject-timeline block;
}
/* 动画元素(可以是不同的元素) */
.animated-element {
animation: my-animation linear;
animation-timeline: --subject-timeline;
}
view-timeline 相关属性:
| 属性 | 说明 |
|---|---|
view-timeline-name | 时间线名称 |
view-timeline-axis | 滚动轴 |
view-timeline-inset | 视口偏移(调整"可见"的定义) |
view-timeline | 简写属性 |
实用示例
滚动驱动的视差效果
.parallax-bg {
animation: parallax linear;
animation-timeline: scroll(root);
}
@keyframes parallax {
from { transform: translateY(0); }
to { transform: translateY(-100px); }
}
元素入场动画
.fade-up {
animation: fadeUp linear;
animation-timeline: view();
animation-range: entry 0% entry 100%; /* 定义动画范围 */
}
@keyframes fadeUp {
from {
opacity: 0;
transform: translateY(50px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
水平滚动进度
.horizontal-progress {
animation: progress linear;
animation-timeline: scroll(inline);
}
@keyframes progress {
from { width: 0; }
to { width: 100%; }
}
animation-range 属性
animation-range 用于精确控制动画在时间线上的生效范围:
/* 语法 */
animation-range: <start> <end>;
/* 值可以是:normal | <length-percentage> | <timeline-range-name> <percentage> */
/* 整个时间线 */
animation-range: normal;
/* 只在时间线的中间 50% 播放 */
animation-range: 25% 75%;
/* 视图时间线专用:entry、exit、cover、contain */
animation-range: entry 0% entry 100%;
animation-range: cover 0% cover 100%;
animation-range: contain 0% contain 100%;
视图时间线范围名称:
| 范围名称 | 说明 |
|---|---|
entry | 元素进入视口的过程 |
exit | 元素离开视口的过程 |
cover | 元素完全覆盖视口的过程 |
contain | 元素完全在视口内的过程 |
/* 只在元素进入视口时播放动画 */
.fade-in {
animation: fadeIn linear;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
/* 只在元素完全可见时播放动画 */
.pulse-when-visible {
animation: pulse linear;
animation-timeline: view();
animation-range: contain 0% contain 100%;
}
注意事项
- animation-duration 设置:滚动驱动动画通常设置
animation-duration: 1ms(Firefox 需要) - 声明顺序:
animation-timeline必须在animation之后声明 - 浏览器支持:使用前检查浏览器兼容性
浏览器支持情况:
| 浏览器 | 支持版本 |
|---|---|
| Chrome | 115+ |
| Edge | 115+ |
| Firefox | 129+ |
| Safari | 17.4+ |
/* 兼容性写法 */
@supports (animation-timeline: scroll()) {
.element {
animation: my-animation linear;
animation-timeline: scroll();
}
}
无障碍考虑
减少动画偏好
尊重用户的系统设置,为不喜欢动画的用户提供替代方案:
/* 用户偏好减少动画时禁用或简化动画 */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
提供暂停控制
对于持续播放的动画,提供暂停方式:
/* 悬停暂停 */
.animated:hover {
animation-play-state: paused;
}
/* 使用按钮控制 */
.animated.paused {
animation-play-state: paused;
}
JavaScript 控制
控制动画
const element = document.querySelector('.animated');
// 暂停动画
element.style.animationPlayState = 'paused';
// 恢复动画
element.style.animationPlayState = 'running';
// 重新开始动画
element.style.animation = 'none';
element.offsetHeight; // 触发重排
element.style.animation = 'slideIn 0.5s ease-out';
监听动画事件
const element = document.querySelector('.animated');
// 动画开始
element.addEventListener('animationstart', () => {
console.log('动画开始');
});
// 动画结束
element.addEventListener('animationend', () => {
console.log('动画结束');
});
// 动画每次循环结束
element.addEventListener('animationiteration', () => {
console.log('一次循环完成');
});
小结
本章学习了:
- @keyframes:定义动画的关键帧和样式变化
- animation 属性:name、duration、timing-function、delay、iteration-count、direction、fill-mode、play-state
- 常用动画效果:淡入淡出、滑入、弹跳、旋转、脉冲等
- 滚动驱动动画:基于滚动位置的动画,包括 scroll() 和 view() 函数
- 性能优化:优先使用 transform 和 opacity
- 无障碍:尊重用户偏好,提供暂停控制
练习
- 创建一个淡入滑入组合动画
- 实现一个加载动画组件
- 使用
prefers-reduced-motion优化无障碍 - 创建一个无限循环的背景渐变动画
- 用 JavaScript 控制动画的播放和暂停
- 使用
scroll()实现一个页面滚动进度指示器 - 使用
view()实现元素滚动入场动画