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) degradturn 顺时针旋转,负值逆时针
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: nonedisplay: 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)、filterwill-changeposition: fixed/stickyisolation: isolate

解决方案:将需要高 z-index 的元素(如 Modal、Tooltip)提升到没有 transform 的祖先元素下,或使用 Portal(Vue Teleport / React createPortal)渲染到 document.body

动画性能调试

使用浏览器 DevTools 的 Performance 面板录制动画,在火焰图中查找是否出现 Layout 和 Paint 任务。绿色为 Composite(合理),紫色为 Layout/Paint(需优化)。

Layers 面板可以查看当前页面的合成层分布,合成层过多(超过几十个)时需要审查 will-changetransform 的使用。


最佳实践

只对 transformopacity 做动画,避免触发 Layout 和 Paint:修改 widthheightmargintop/left 等属性会触发 Layout 重排,性能极差。transform: translate/scale/rotateopacity 只触发 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')
})

参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog