跳到主要内容

CSS 动画

CSS 动画让我们可以在不使用 JavaScript 的情况下创建流畅的动画效果。本章将详细介绍 CSS 动画的完整用法。

动画基础概念

什么是 CSS 动画?

CSS 动画让元素从一个样式逐渐变化到另一个样式。与 CSS 过渡(transition)相比,动画有以下优势:

  • 可以创建多步骤的动画序列
  • 可以循环播放
  • 更精细的控制(暂停、反向播放等)
  • 不需要触发条件(如 hover)

动画的两个部分

创建 CSS 动画需要两部分配合:

  1. @keyframes 规则:定义动画的关键帧和样式变化
  2. animation 属性:将动画应用到元素上
/* 第一部分:定义关键帧 */
@keyframes slideIn {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}

/* 第二部分:应用动画 */
.element {
animation: slideIn 0.5s ease-out;
}

@keyframes 关键帧

基本语法

@keyframes 规则定义动画的中间步骤:

@keyframes 动画名称 {
关键帧选择器 {
样式属性:;
}
}

from 和 to

使用 fromto 定义动画的起点和终点:

@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;
}
慎用 will-change

只在需要时使用,过度使用会消耗更多资源。

避免动画盒模型属性

/* 不推荐:动画 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%;
}

注意事项

  1. animation-duration 设置:滚动驱动动画通常设置 animation-duration: 1ms(Firefox 需要)
  2. 声明顺序animation-timeline 必须在 animation 之后声明
  3. 浏览器支持:使用前检查浏览器兼容性

浏览器支持情况:

浏览器支持版本
Chrome115+
Edge115+
Firefox129+
Safari17.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('一次循环完成');
});

小结

本章学习了:

  1. @keyframes:定义动画的关键帧和样式变化
  2. animation 属性:name、duration、timing-function、delay、iteration-count、direction、fill-mode、play-state
  3. 常用动画效果:淡入淡出、滑入、弹跳、旋转、脉冲等
  4. 滚动驱动动画:基于滚动位置的动画,包括 scroll() 和 view() 函数
  5. 性能优化:优先使用 transform 和 opacity
  6. 无障碍:尊重用户偏好,提供暂停控制

练习

  1. 创建一个淡入滑入组合动画
  2. 实现一个加载动画组件
  3. 使用 prefers-reduced-motion 优化无障碍
  4. 创建一个无限循环的背景渐变动画
  5. 用 JavaScript 控制动画的播放和暂停
  6. 使用 scroll() 实现一个页面滚动进度指示器
  7. 使用 view() 实现元素滚动入场动画