CSS 动画完全指南
CSS 动画体系由三个部分构成:transition(过渡)、@keyframes + animation(关键帧动画)、transform(变换)。三者经常配合使用。 transition 用于在 CSS 属性值发生变化时产生平滑过渡效果。 transition 是以下四个子属性的简写: transition 需要状态变化才能触发,常见触发方式: @keyframes 定义动画序列,animation 属性将其应用到元素上。与 transition 的区别在于:动画可以自动播放、循环、有更复杂的中间状态。 animation 是以下子属性的简写: fi
官方文档:https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_animations
适用版本:CSS Level 3 Animations(2026-05-07 整理)
CSS 动画体系由三个部分构成:transition(过渡)、@keyframes + animation(关键帧动画)、transform(变换)。三者经常配合使用。
transition 过渡
transition 用于在 CSS 属性值发生变化时产生平滑过渡效果。
transition 属性
transition 是以下四个子属性的简写:
| 子属性 | 说明 | 示例值 |
|---|---|---|
transition-property |
要过渡的 CSS 属性名,all 表示所有可动画属性 |
opacity, transform, all |
transition-duration |
过渡持续时长 | 0.3s, 300ms |
transition-timing-function |
速度曲线(缓动函数) | ease, linear, cubic-bezier(...) |
transition-delay |
过渡开始前的延迟时间 | 0s, 0.1s |
/* 简写顺序:property duration timing-function delay */
.button {
transition: background-color 0.3s ease 0s;
}
/* 多属性过渡,用逗号分隔 */
.card {
transition:
transform 0.3s ease,
box-shadow 0.3s ease,
opacity 0.2s linear;
}
timing-function 缓动函数
| 值 | 说明 | 对应 cubic-bezier |
|---|---|---|
linear |
匀速 | cubic-bezier(0, 0, 1, 1) |
ease |
默认值,先快后慢 | cubic-bezier(0.25, 0.1, 0.25, 1) |
ease-in |
由慢到快(加速进入) | cubic-bezier(0.42, 0, 1, 1) |
ease-out |
由快到慢(减速结束) | cubic-bezier(0, 0, 0.58, 1) |
ease-in-out |
两端慢,中间快 | cubic-bezier(0.42, 0, 0.58, 1) |
cubic-bezier(x1,y1,x2,y2) |
自定义贝塞尔曲线 | — |
steps(n, start|end) |
分 n 步跳变,无平滑过渡 | — |
/* 自定义弹性效果 */
.spring {
transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* 打字机效果(配合 steps) */
.typewriter {
transition: width 2s steps(20, end);
}
触发条件
transition 需要状态变化才能触发,常见触发方式:
/* 1. :hover 伪类 */
.button {
background-color: blue;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: darkblue;
}
/* 2. :focus 伪类 */
.input {
border-color: #ccc;
transition: border-color 0.2s ease;
}
.input:focus {
border-color: blue;
}
// 3. JavaScript 动态切换 class
element.classList.toggle('is-open')
// 4. JavaScript 直接修改 style
element.style.opacity = '0'
/* 对应的过渡定义写在元素初始状态上 */
.panel {
opacity: 1;
transform: translateY(0);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.panel.is-open {
opacity: 0;
transform: translateY(-10px);
}
@keyframes 动画
@keyframes 定义动画序列,animation 属性将其应用到元素上。与 transition 的区别在于:动画可以自动播放、循环、有更复杂的中间状态。
@keyframes 语法
/* from/to 写法(等同于 0% / 100%) */
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 百分比写法,支持任意中间帧 */
@keyframes bounce {
0% { transform: translateY(0); }
40% { transform: translateY(-20px); }
60% { transform: translateY(-10px); }
80% { transform: translateY(-5px); }
100% { transform: translateY(0); }
}
animation 属性
animation 是以下子属性的简写:
| 子属性 | 说明 | 示例值 |
|---|---|---|
animation-name |
@keyframes 的名称 |
fade-in, bounce |
animation-duration |
一次动画的持续时长 | 0.5s, 1s |
animation-timing-function |
速度曲线(同 transition) | ease, linear |
animation-delay |
动画开始前的延迟 | 0s, 0.2s |
animation-iteration-count |
播放次数,infinite 无限循环 |
1, 3, infinite |
animation-direction |
播放方向 | normal, reverse, alternate |
animation-fill-mode |
动画前后的状态 | none, forwards, backwards, both |
animation-play-state |
播放或暂停 | running, paused |
/* 简写:name duration timing-function delay iteration-count direction fill-mode */
.element {
animation: fade-in 0.5s ease 0.1s 1 normal forwards;
}
/* 多个动画同时应用 */
.loader {
animation:
spin 1s linear infinite,
pulse 2s ease-in-out infinite;
}
animation-fill-mode 详解
fill-mode 控制动画在执行前(delay 期间)和执行后的状态:
| 值 | delay 期间 | 动画结束后 | 说明 |
|---|---|---|---|
none |
元素原始样式 | 元素原始样式 | 默认值,动画结束后复位 |
forwards |
元素原始样式 | 保持最后一帧(100%)的样式 |
最常用,保持动画结束状态 |
backwards |
应用第一帧(0%)的样式 |
元素原始样式 | 在 delay 期间就应用初始帧 |
both |
应用第一帧的样式 | 保持最后一帧的样式 | forwards + backwards |
/* 元素进入后保持显示状态(最常用场景) */
.modal {
animation: slide-in 0.3s ease forwards;
}
@keyframes slide-in {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
animation-direction 详解
| 值 | 说明 |
|---|---|
normal |
每次从头播到尾(0% → 100%) |
reverse |
每次从尾播到头(100% → 0%) |
alternate |
奇数次正向,偶数次反向(来回);常配合 infinite 使用 |
alternate-reverse |
奇数次反向,偶数次正向 |
/* 呼吸灯效果 */
.pulse {
animation: opacity-pulse 2s ease-in-out infinite alternate;
}
@keyframes opacity-pulse {
from { opacity: 0.4; }
to { opacity: 1; }
}
Transform 变换
transform 对元素进行几何变换,不会触发 layout reflow,性能好。
2D 变换
| 函数 | 参数 | 说明 |
|---|---|---|
translate(x, y) |
长度值,可为负 | 平移,translateX(x) / translateY(y) 为单轴简写 |
rotate(angle) |
deg、rad、turn |
顺时针旋转,负值逆时针 |
scale(x, y) |
数字(倍率) | 缩放,scale(1.5) 等比放大 1.5 倍,scaleX() / scaleY() 单轴 |
skew(x, y) |
deg |
倾斜(错切变换),skewX() / skewY() 单轴 |
matrix(a,b,c,d,e,f) |
6 个数字 | 2D 变换矩阵,包含以上所有变换 |
.card:hover {
transform: translateY(-4px) scale(1.02);
}
.icon {
transform: rotate(45deg);
}
/* 多个变换按顺序叠加,顺序影响结果 */
.element {
/* 先旋转再平移,与先平移再旋转结果不同 */
transform: rotate(30deg) translate(50px, 0);
}
3D 变换
| 函数 | 说明 |
|---|---|
rotateX(angle) |
绕 X 轴旋转(上下翻转) |
rotateY(angle) |
绕 Y 轴旋转(左右翻转,卡片翻转效果) |
rotateZ(angle) |
绕 Z 轴旋转(等同于 2D rotate()) |
translateZ(z) |
沿 Z 轴平移(靠近/远离视点) |
scaleZ(z) |
沿 Z 轴缩放(仅影响 3D 变换效果) |
perspective(length) |
设置透视距离(值越小透视越强烈) |
rotate3d(x, y, z, angle) |
绕任意向量轴旋转 |
/* 卡片翻转效果 */
.card-container {
perspective: 800px; /* 在父元素设置透视 */
}
.card {
transform-style: preserve-3d;
transition: transform 0.6s ease;
}
.card:hover {
transform: rotateY(180deg);
}
.card-front,
.card-back {
backface-visibility: hidden; /* 隐藏背面 */
}
.card-back {
transform: rotateY(180deg);
}
也可以用 perspective() 函数直接写在 transform 中(仅影响该元素本身):
.element {
transform: perspective(500px) rotateY(30deg);
}
transform-origin 变换原点
| 值 | 说明 |
|---|---|
50% 50%(默认) |
元素中心 |
top left / 0 0 |
左上角 |
bottom right / 100% 100% |
右下角 |
50% 100% |
底部中心(折叠展开效果) |
x y z |
3D 变换可指定三个值 |
/* 从左上角旋转(报纸折叠效果) */
.fold {
transform-origin: top left;
transform: rotateX(-90deg);
}
/* 从底部展开 */
.dropdown {
transform-origin: top center;
transform: scaleY(0);
transition: transform 0.3s ease;
}
.dropdown.open {
transform: scaleY(1);
}
实战动画模式
进入/离开动画
配合 Vue <Transition> 组件:
<template>
<Transition name="fade">
<div v-if="show" class="modal">内容</div>
</Transition>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(-8px);
}
</style>
配合 React 状态切换(使用 class 方式):
'use client'
import { useState, useEffect } from 'react'
export function FadeInOut({ visible }: { visible: boolean }) {
const [mounted, setMounted] = useState(visible)
useEffect(() => {
if (visible) setMounted(true)
}, [visible])
return mounted ? (
<div
className={`modal ${visible ? 'modal--visible' : 'modal--hidden'}`}
onTransitionEnd={() => { if (!visible) setMounted(false) }}
/>
) : null
}
.modal {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.modal--visible {
opacity: 1;
transform: translateY(0);
}
.modal--hidden {
opacity: 0;
transform: translateY(-8px);
}
加载 Spinner
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 24px;
height: 24px;
border: 3px solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
骨架屏
@keyframes skeleton-shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: 4px;
}
.skeleton-title { height: 20px; width: 60%; margin-bottom: 12px; }
.skeleton-text { height: 14px; width: 100%; margin-bottom: 8px; }
无限滚动跑马灯
@keyframes marquee {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
.marquee-container {
overflow: hidden;
white-space: nowrap;
}
.marquee-track {
display: inline-flex;
/* 内容复制两份,宽度为 200%,动画滚动 50% 后无缝衔接 */
animation: marquee 20s linear infinite;
}
.marquee-track:hover {
animation-play-state: paused;
}
点击涟漪效果
.ripple-button {
position: relative;
overflow: hidden;
}
@keyframes ripple-expand {
to {
transform: scale(4);
opacity: 0;
}
}
.ripple {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
width: 60px;
height: 60px;
margin-top: -30px;
margin-left: -30px;
animation: ripple-expand 0.6s linear;
pointer-events: none;
}
function createRipple(event) {
const button = event.currentTarget
const circle = document.createElement('span')
const rect = button.getBoundingClientRect()
circle.classList.add('ripple')
circle.style.left = `${event.clientX - rect.left}px`
circle.style.top = `${event.clientY - rect.top}px`
button.appendChild(circle)
circle.addEventListener('animationend', () => circle.remove())
}
性能优化
只动画 transform 和 opacity
浏览器渲染流水线分三层:Layout(布局)、Paint(绘制)、Composite(合成)。
| 动画属性 | 触发阶段 | 性能 |
|---|---|---|
width, height, margin, padding, top, left 等 |
Layout + Paint + Composite | 差,会导致整页重排 |
color, background-color, border-color, box-shadow |
Paint + Composite | 中,不重排但需重绘 |
transform, opacity |
Composite only | 好,GPU 加速合成层 |
/* 推荐:使用 transform 实现位移,而非修改 top/left */
.card {
transition: transform 0.3s ease; /* 好 */
}
.card:hover {
transform: translateY(-4px); /* 好:只触发 Composite */
}
/* 不推荐 */
.card {
transition: top 0.3s ease; /* 差:触发 Layout */
position: relative;
top: 0;
}
.card:hover {
top: -4px;
}
will-change
will-change 提前通知浏览器该元素即将发生变换,浏览器会提前为其创建合成层。
| 使用场景 | 说明 |
|---|---|
| 复杂动画元素 | 动画开始前设置,动画结束后移除 |
| 频繁变换的元素 | 如拖拽、吸顶 Header |
/* 基本用法 */
.heavy-animation {
will-change: transform, opacity;
}
注意事项:
- 不要对所有元素设置
will-change。每个合成层都消耗内存(GPU VRAM),过度使用反而降低性能。 - 静态元素不需要设置,只在确实需要优化的动画元素上使用。
- 最佳实践是用 JavaScript 在动画开始前设置,结束后移除:
element.addEventListener('mouseenter', () => {
element.style.willChange = 'transform'
})
element.addEventListener('animationend', () => {
element.style.willChange = 'auto'
})
避免触发 Layout Reflow 的属性
以下属性变化会触发 layout reflow,动画中应避免:
| 类别 | 属性 |
|---|---|
| 尺寸 | width, height, min-width, max-width, min-height, max-height |
| 内外边距 | margin, padding, border-width |
| 定位 | top, right, bottom, left |
| 字体 | font-size, font-family, line-height |
| 布局相关 | display, position, float, overflow |
用 transform 代替位置属性,用 opacity 代替 visibility / display 切换。
踩坑与注意事项
display: none 无法过渡
display 属性不是可动画属性,从 display: none 到 display: block 没有过渡效果。
/* 错误:不会有过渡 */
.panel {
display: none;
transition: opacity 0.3s;
}
.panel.show {
display: block;
opacity: 1;
}
/* 正确方案 1:使用 opacity + pointer-events 替代 display */
.panel {
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.panel.show {
opacity: 1;
pointer-events: auto;
}
/* 正确方案 2:使用 visibility(可动画)配合 opacity */
.panel {
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.panel.show {
opacity: 1;
visibility: visible;
}
CSS 新特性(Chrome 117+)支持对 display 使用过渡,需要配合 @starting-style:
/* 现代浏览器:display 过渡(实验性) */
.panel {
display: none;
opacity: 0;
transition: opacity 0.3s, display 0.3s allow-discrete;
}
.panel.show {
display: block;
opacity: 1;
@starting-style {
opacity: 0;
}
}
transform 创建新的 stacking context
应用了 transform(非 none)的元素会创建新的层叠上下文(stacking context),影响 z-index 的行为。
/* 问题场景:父元素有 transform,子元素的 z-index 无法超出父元素 */
.parent {
transform: translateZ(0); /* 创建了新的 stacking context */
}
.child {
z-index: 9999; /* 只在 parent 的 stacking context 内有效 */
}
.tooltip {
z-index: 100; /* 如果 tooltip 在 parent 外部,会显示在 child 上方 */
}
同样会创建新 stacking context 的属性:opacity(小于 1)、filter、will-change、position: fixed/sticky、isolation: isolate。
解决方案:将需要高 z-index 的元素(如 Modal、Tooltip)提升到没有 transform 的祖先元素下,或使用 Portal(Vue Teleport / React createPortal)渲染到 document.body。
动画性能调试
使用浏览器 DevTools 的 Performance 面板录制动画,在火焰图中查找是否出现 Layout 和 Paint 任务。绿色为 Composite(合理),紫色为 Layout/Paint(需优化)。
Layers 面板可以查看当前页面的合成层分布,合成层过多(超过几十个)时需要审查 will-change 和 transform 的使用。
最佳实践
只对 transform 和 opacity 做动画,避免触发 Layout 和 Paint:修改 width、height、margin、top/left 等属性会触发 Layout 重排,性能极差。transform: translate/scale/rotate 和 opacity 只触发 Composite,由 GPU 处理,是唯一推荐的动画属性。
用 will-change: transform 提前创建合成层,但不要滥用:对即将动画的元素提前声明 will-change 可减少首帧抖动,但每个合成层都消耗额外 GPU 内存,不应对页面上所有元素都添加。仅对确实会动画的关键元素使用,动画结束后移除。
动画使用 animation-fill-mode: forwards 保持结束状态:默认情况下动画结束后元素回到初始样式,forwards 让元素保持 @keyframes 最后一帧的样式,避免需要用 JavaScript 在动画结束时设置最终状态。
过渡只写在稳定状态(非 hover),避免反向过渡失效:把 transition 写在 :hover 上时,鼠标离开后元素立即跳回,不会有过渡效果(因为非 hover 状态没有 transition)。应把 transition 写在基础类选择器上。
复杂序列动画用 animation-delay 错开时间,而非多个 setTimeout:CSS 动画时序由浏览器精确控制,比 JS 定时器更稳定,不受主线程阻塞影响。
常见陷阱
陷阱:transform 导致后代元素的 position: fixed 失效
现象: 弹窗、Tooltip 等 position: fixed 的元素被 transform 祖先元素遮挡,或其 z-index 无法突破祖先层级。
原因: 对任何祖先元素应用 transform(包括 translate(0,0) 这种"无效"变换)会创建新的包含块,使后代的 position: fixed 相对于该祖先定位而非视口。
解决: 将 fixed 元素提升到没有 transform 的祖先元素之外,或用 Vue Teleport / React Portal 渲染到 body 根节点。
陷阱:动画结束后元素跳回初始位置
现象: 执行完向右移动的动画后,元素突然跳回原位置。
原因: animation-fill-mode 默认值为 none,动画结束后样式回退到动画前状态。
解决: 设置 animation-fill-mode: forwards 保持最终帧样式;若需双向保持,用 both。
陷阱:transition 在元素首次渲染时意外触发
现象: 页面加载后,元素从初始位置滑入,用户没有交互但动画自动播放。
原因: 元素带有 transition 时,若 JavaScript 在 DOMContentLoaded 后立即修改了样式(如添加 class),浏览器会将从无到有的样式变化解读为过渡动画。
解决: 将样式修改推迟一帧(requestAnimationFrame),让浏览器先完成初始渲染,再应用 transition。
requestAnimationFrame(() => {
element.classList.add('visible')
})