> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# CSS 动画完全指南
- URL: https://blog.vercanti.com/css-dong-hua-wan-quan-zhi-nan/
- Published: 2026-08-28T14:35:13.000Z
- Updated: 2026-08-28T14:58:21.000Z
- Description: CSS 动画体系由三个部分构成：transition（过渡）、@keyframes + animation（关键帧动画）、transform（变换）。三者经常配合使用。 transition 用于在 CSS 属性值发生变化时产生平滑过渡效果。 transition 是以下四个子属性的简写： transition 需要状态变化才能触发，常见触发方式： @keyframes 定义动画序列，animation 属性将其应用到元素上。与 transition 的区别在于：动画可以自动播放、循环、有更复杂的中间状态。 animation 是以下子属性的简写： fi
- Author: yellowdog
- Tags: 前端开发

> 官方文档：<https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS%5Fanimations>  
> 适用版本：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                        |

```css
/* 简写顺序：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 步跳变，无平滑过渡 | —                                |

```css
/* 自定义弹性效果 */
.spring {
  transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* 打字机效果（配合 steps） */
.typewriter {
  transition: width 2s steps(20, end);
}

```

### 触发条件

`transition` 需要状态变化才能触发，常见触发方式：

```css
/* 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;
}

```

```javascript
// 3. JavaScript 动态切换 class
element.classList.toggle('is-open')

// 4. JavaScript 直接修改 style
element.style.opacity = '0'

```

```css
/* 对应的过渡定义写在元素初始状态上 */
.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 语法

```css
/* 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                 |

```css
/* 简写：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 |

```css
/* 元素进入后保持显示状态（最常用场景） */
.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 | 奇数次反向，偶数次正向                     |

```css
/* 呼吸灯效果 */
.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 变换矩阵，包含以上所有变换                                |

```css
.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) | 绕任意向量轴旋转                 |

```css
/* 卡片翻转效果 */
.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` 中（仅影响该元素本身）：

```css
.element {
  transform: perspective(500px) rotateY(30deg);
}

```

### transform-origin 变换原点

| 值                        | 说明           |
| ------------------------ | ------------ |
| 50% 50%（默认）              | 元素中心         |
| top left / 0 0           | 左上角          |
| bottom right / 100% 100% | 右下角          |
| 50% 100%                 | 底部中心（折叠展开效果） |
| x y z                    | 3D 变换可指定三个值  |

```css
/* 从左上角旋转（报纸折叠效果） */
.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>` 组件：

```vue
<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 方式）：

```tsx
'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
}

```

```css
.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

```css
@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;
}

```

### 骨架屏

```css
@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; }

```

### 无限滚动跑马灯

```css
@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;
}

```

### 点击涟漪效果

```css
.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;
}

```

```javascript
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 加速合成层 |

```css
/* 推荐：使用 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   |

```css
/* 基本用法 */
.heavy-animation {
  will-change: transform, opacity;
}

```

注意事项：

- 不要对所有元素设置 `will-change`。每个合成层都消耗内存（GPU VRAM），过度使用反而降低性能。
- 静态元素不需要设置，只在确实需要优化的动画元素上使用。
- 最佳实践是用 JavaScript 在动画开始前设置，结束后移除：

```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` 没有过渡效果。

```css
/* 错误：不会有过渡 */
.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`：

```css
/* 现代浏览器：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` 的行为。

```css
/* 问题场景：父元素有 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。

```javascript
requestAnimationFrame(() => {
  element.classList.add('visible')
})

```

---

## 参见

- [HTML与CSS入门](https://blog.vercanti.com/html-yu-css-ru-men/)
- [TailwindCSS完全指南](https://blog.vercanti.com/tailwindcss-wan-quan-zhi-nan/)
- [Vue3入门](https://blog.vercanti.com/vue-3-ru-men-zhi-nan/)