CSS 动画与过渡效果

中等 🟡CSS 布局
7 个标签
预计阅读时间:41 分钟
CSS动画transitionanimationkeyframes性能优化GPU 加速

CSS 动画与过渡效果

CSS 动画和过渡为网页增添交互性和视觉吸引力,是提升用户体验、传达界面状态的关键手段。一个恰到好处的按钮反馈、一段流畅的页面入场,往往比十行文案更能让用户感受到产品的"质感"。

为什么需要动画

界面动画不只是"好看",它承担着实实在在的功能:

1.提供反馈:按钮按下时轻微下沉,告诉用户"点击已被接收"。
2.引导注意力:新消息用脉冲高亮,把用户视线拉过去。
3.建立空间连续性:弹窗从触发按钮位置放大出现,用户能理解元素从哪来、到哪去。
4.掩盖等待:加载动画让 300ms 的接口等待感觉不那么漫长。

据 Google Material Design 团队的研究,界面过渡时长控制在 200~500ms 之间时,用户感知最自然;低于 100ms 用户几乎察觉不到,高于 500ms 则开始感到拖沓。

过渡(Transition)与动画(Animation)的区别

这是初学者最容易混淆的一对概念:

| 维度 | Transition(过渡) | Animation(动画) |

|---|---|---|

| 触发方式 | 需状态改变触发(如 :hover) | 可自动播放,无需触发 |

| 关键帧 | 只有起点和终点两个状态 | 可定义任意多个中间关键帧 |

| 循环 | 不能循环 | 可无限循环 |

| 方向控制 | 不支持往返 | 支持 alternate 往返 |

| 适用场景 | 简单的状态切换 | 复杂、连续、循环的动效 |

一句话总结:过渡是"从 A 到 B",动画是"A→B→C→D 甚至循环往复"

过渡效果(Transition)

基本属性:

transition-property:指定要过渡的 CSS 属性(如 all、opacity)
transition-duration:过渡持续时间(如 0.3s)
transition-timing-function:时间函数(缓动曲线)
transition-delay:延迟多久后开始
transition:以上四者的简写属性

时间函数(缓动曲线):

ease:默认值,慢-快-慢,最自然
linear:匀速,适合旋转类动画
ease-in:慢开始,适合退场
ease-out:慢结束,适合入场
ease-in-out:慢-快-慢,适合往返
cubic-bezier():自定义贝塞尔曲线,可做出弹性、回弹等效果
steps():分步跳变,适合逐帧动画(如打字机、精灵图)

可过渡的属性与不可过渡的属性:

可过渡:width、height、color、background-color、opacity、transform、box-shadow、filter 等有"中间值"的属性
不可过渡:display、visibility、position 等离散型属性(没有中间状态)

💻 代码示例:按钮过渡效果

cssCode
.button {
  padding: 12px 24px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  /* 对所有可过渡属性应用 0.3s 缓动 */
  transition: all 0.3s ease;
}

.button:hover {
  background-color: #0056b3;
  transform: translateY(-2px);           /* 轻微上浮 */
  box-shadow: 0 4px 8px rgba(0,0,0,0.2); /* 阴影加深,制造悬浮感 */
}

.button:active {
  transform: translateY(0);              /* 按下回落 */
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

💻 代码示例:精细控制多属性过渡

对不同属性设置不同时长和缓动,能做出更精致的效果。注意:过渡多个明确属性比 `transition: all` 性能更好,因为浏览器无需监听所有属性。

cssCode
.card {
  transform: scale(1);
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  /* 分别为 transform 和 box-shadow 指定不同参数 */
  transition:
    transform 0.2s ease-out,
    box-shadow 0.4s ease-in-out;
}

.card:hover {
  transform: scale(1.03);
  box-shadow: 0 12px 24px rgba(0,0,0,0.18);
}

/* 自定义弹性曲线:结束时轻微回弹 */
.bouncy {
  transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.bouncy:hover {
  transform: scale(1.1);
}

关键帧动画(Animation)

基本属性:

animation-name:绑定的 @keyframes 名称
animation-duration:单次播放时长
animation-timing-function:缓动函数
animation-delay:延迟开始时间
animation-iteration-count:播放次数(数字或 infinite)
animation-direction:播放方向(normal / reverse / alternate)
animation-fill-mode:动画前后的填充状态(forwards / backwards / both)
animation-play-state:播放/暂停(running / paused)
animation:简写属性

@keyframes 规则:

用 from/to 定义两端,或用百分比定义任意多个关键帧
未在关键帧中声明的属性,浏览器会自动插值

常用控制关键字:

infinite:无限循环
alternate:往返播放(正放一次、倒放一次)
forwards:动画结束后停留在最后一帧
paused:暂停在当前帧

💻 代码示例:加载动画

cssCode
/* 旋转加载器:linear 保证匀速旋转 */
.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #007bff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

/* 脉冲加载器:用扩散阴影营造心跳感 */
.pulse {
  width: 40px;
  height: 40px;
  background-color: #007bff;
  border-radius: 50%;
  animation: pulse 1.5s ease-in-out infinite;
}

@keyframes pulse {
  0% {
    transform: scale(0.95);
    box-shadow: 0 0 0 0 rgba(0, 123, 255, 0.7);
  }
  70% {
    transform: scale(1);
    box-shadow: 0 0 0 10px rgba(0, 123, 255, 0);
  }
  100% {
    transform: scale(0.95);
    box-shadow: 0 0 0 0 rgba(0, 123, 255, 0);
  }
}

💻 代码示例:骨架屏与打字机效果

cssCode
/* 骨架屏微光扫过效果:内容加载前的占位动画 */
.skeleton {
  background: linear-gradient(
    90deg,
    #f0f0f0 25%,
    #e0e0e0 37%,
    #f0f0f0 63%
  );
  background-size: 400% 100%;
  animation: shimmer 1.4s ease infinite;
}

@keyframes shimmer {
  0%   { background-position: 100% 0; }
  100% { background-position: 0 0; }
}

/* 打字机效果:用 steps() 做逐字符跳变 */
.typewriter {
  width: 22ch;
  white-space: nowrap;
  overflow: hidden;
  border-right: 2px solid #333;
  animation:
    typing 3s steps(22, end),
    blink 0.7s step-end infinite;
}

@keyframes typing {
  from { width: 0; }
  to   { width: 22ch; }
}

@keyframes blink {
  50% { border-color: transparent; }
}

💻 代码示例:入场动画与 fill-mode

cssCode
/* 元素从下方淡入,结束后停留在最终状态 */
.fade-in-up {
  opacity: 0;
  animation: fadeInUp 0.6s ease-out forwards; /* forwards 保持结束帧 */
  animation-delay: 0.2s;
}

@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(24px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* 列表逐项进场:配合不同 delay 形成瀑布式入场 */
.list-item:nth-child(1) { animation-delay: 0.05s; }
.list-item:nth-child(2) { animation-delay: 0.10s; }
.list-item:nth-child(3) { animation-delay: 0.15s; }
.list-item:nth-child(4) { animation-delay: 0.20s; }

动画性能:重排、重绘与合成

理解浏览器的渲染流水线,是写出流畅动画的核心。浏览器渲染大致分三步:

1.Layout(重排/回流):计算元素几何位置。改 width、height、top、margin 会触发,代价最高。
2.Paint(重绘):填充像素。改 color、background、box-shadow 会触发,代价中等。
3.Composite(合成):把图层拼合。只改 transform、opacity 时,可以跳过前两步,直接由 GPU 合成,代价最低。

结论:动画尽量只改 transform 和 opacity。 它们能被提升到独立的合成层,交由 GPU 处理,轻松跑满 60fps。

| 动画属性 | 触发阶段 | 性能 | 建议 |

|---|---|---|---|

| transform | 仅合成 | 极佳 | 首选(位移/缩放/旋转) |

| opacity | 仅合成 | 极佳 | 首选(淡入淡出) |

| filter | 重绘+合成 | 良好 | 谨慎使用 |

| box-shadow | 重绘 | 一般 | 用 transform 阴影层替代 |

| width/height | 重排 | 差 | 改用 transform: scale |

| top/left | 重排 | 差 | 改用 transform: translate |

💻 代码示例:动画性能优化

cssCode
/* 使用 GPU 加速的属性组合 */
.gpu-accelerated {
  transform: translate3d(0, 0, 0); /* 提升为合成层 */
  opacity: 1;
}

/* will-change 提前告知浏览器:这个元素将要动画 */
.animated-element {
  will-change: transform, opacity;
  animation: move 2s ease-in-out;
}

@keyframes move {
  0%   { transform: translateX(0); }
  100% { transform: translateX(100px); }
}

/* 动画完成后务必移除 will-change,否则长期占用内存 */
.animated-element.finished {
  will-change: auto;
}

/* 反例:用 left 做位移会触发重排,掉帧 */
.bad-performance {
  position: relative;
  left: 0;
  transition: left 0.3s; /* ❌ 每帧都重排 */
}

/* 正例:等效效果用 transform,仅合成 */
.good-performance {
  transition: transform 0.3s; /* ✅ 只在合成层运行 */
}
.good-performance:hover {
  transform: translateX(100px);
}

💻 代码示例:尊重用户的动画偏好

无障碍访问的重要一环——部分用户会因前庭功能障碍对动画敏感,操作系统提供了"减弱动态效果"开关,CSS 应予以尊重。

cssCode
/* 尊重系统级"减少动态效果"设置 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* 或针对特定元素完全关闭动画 */
@media (prefers-reduced-motion: reduce) {
  .parallax-banner {
    animation: none;
    transition: none;
  }
}

真实案例:登录按钮的加载态优化

场景: 某后台系统登录按钮,点击后要等待接口约 800ms。原本按钮点击后毫无反馈,用户以为没点上,反复点击导致重复提交。

方案: 点击后按钮宽度收缩、文字淡出、内嵌旋转 spinner,成功后再展开。关键是全程用 transform + opacity,避免因宽度动画触发重排。

cssCode
.login-btn {
  transition: transform 0.2s ease, opacity 0.2s ease;
}
.login-btn.is-loading {
  pointer-events: none; /* 禁止重复点击 */
  opacity: 0.85;
}
.login-btn.is-loading .btn-text {
  opacity: 0;
}
.login-btn.is-loading .btn-spinner {
  opacity: 1;
  animation: spin 0.8s linear infinite;
}

效果数据: 上线后重复提交率下降约 92%,用户对"系统响应速度"的主观评分从 3.2 提升到 4.5(5 分制),而接口耗时本身并未改变——这就是动画掩盖等待的价值。

深入:贝塞尔曲线的原理

所有缓动函数本质上都是一条描述"进度 vs 时间"的曲线。cubic-bezier(x1, y1, x2, y2) 用两个控制点定义这条曲线:横轴是时间(0→1),纵轴是动画进度(0→1)。当 y 超过 1 或小于 0 时,就会产生"过冲回弹"的弹性效果。

cssCode
/* 常见缓动曲线的贝塞尔等价写法 */
.ease        { transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1); }
.ease-in     { transition-timing-function: cubic-bezier(0.42, 0, 1, 1); }
.ease-out    { transition-timing-function: cubic-bezier(0, 0, 0.58, 1); }
.ease-in-out { transition-timing-function: cubic-bezier(0.42, 0, 0.58, 1); }

/* 自定义:Material Design 标准曲线 */
.md-standard { transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
/* 强调曲线:快进慢出,突出重点元素 */
.md-emphasis { transition-timing-function: cubic-bezier(0.0, 0, 0.2, 1); }
/* 回弹曲线:y 超过 1,结束时越界再弹回 */
.overshoot   { transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); }

深入:steps() 与精灵图逐帧动画

steps(n) 让动画在 n 个离散时刻跳变,而非连续插值,非常适合用一张"精灵图"(sprite sheet)播放逐帧动画——把多帧画面横向拼成一张长图,用 background-position 逐格移动。

cssCode
/* 精灵图逐帧动画:一张 800px 宽的图含 8 帧,每帧 100px */
.sprite {
  width: 100px;
  height: 100px;
  background: url('/walk-sprite.png') 0 0 / 800px 100px;
  /* steps(8) 让背景位置在 8 个位置间跳变,形成逐帧播放 */
  animation: walk 0.8s steps(8) infinite;
}

@keyframes walk {
  from { background-position: 0 0; }
  to   { background-position: -800px 0; } /* 移过整张图 */
}

交错动画(Stagger)与负延迟技巧

给一组元素设置递增的 animation-delay,能形成优雅的"波浪式"入场。而负的 animation-delay 是一个巧妙技巧:让动画像"已经播放了一段时间"一样立即从中途开始,常用于让多个循环动画错开相位。

cssCode
/* 三个圆点的加载动画,用负延迟错开相位形成跳动波浪 */
.dot { animation: bounce 1.2s ease-in-out infinite; }
.dot:nth-child(1) { animation-delay: -0.32s; }
.dot:nth-child(2) { animation-delay: -0.16s; }
.dot:nth-child(3) { animation-delay: 0s; }

@keyframes bounce {
  0%, 80%, 100% { transform: scale(0); }
  40%           { transform: scale(1); }
}

/* 用 CSS 变量批量生成交错延迟(配合内联 style 传入 --i) */
.stagger-item {
  animation: fadeInUp 0.5s ease-out backwards;
  animation-delay: calc(var(--i) * 60ms);
}

3D 变换与透视

transform 不止于 2D。配合 perspective,可以做出翻牌、立方体旋转等 3D 效果,且同样运行在合成层,性能优异。

cssCode
/* 翻牌效果:鼠标悬停时卡片沿 Y 轴翻转 180 度 */
.flip-card {
  perspective: 1000px; /* 透视距离,越小透视越夸张 */
}
.flip-card__inner {
  position: relative;
  transform-style: preserve-3d;      /* 保留子元素的 3D 空间 */
  transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.flip-card:hover .flip-card__inner {
  transform: rotateY(180deg);
}
.flip-card__front,
.flip-card__back {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;       /* 隐藏背面,翻转时才不穿帮 */
}
.flip-card__back {
  transform: rotateY(180deg);
}

现代动画新特性

CSS 动画能力仍在快速进化,以下特性大幅减少了对 JS 的依赖:

cssCode
/* 1. 滚动驱动动画:进度条随页面滚动填充,无需 JS 监听 scroll */
.progress-bar {
  animation: grow linear;
  animation-timeline: scroll(root block); /* 绑定到根滚动条 */
}
@keyframes grow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* 2. 元素进入视口时触发动画(view-timeline) */
.reveal {
  animation: fadeInUp 0.6s ease-out both;
  animation-timeline: view();
  animation-range: entry 0% cover 30%;
}

/* 3. 离散属性过渡:让 display:none 也能优雅淡出 */
.dialog {
  transition: opacity 0.3s, display 0.3s allow-discrete;
}
@starting-style {
  .dialog[open] { opacity: 0; } /* 定义进入前的起始态 */
}

CSS 动画 vs JavaScript 动画

| 维度 | CSS 动画 | JS 动画(如 GSAP / WAAPI) |

|---|---|---|

| 性能 | 合成层动画由 GPU 处理,主线程空闲 | 依赖实现,requestAnimationFrame 较好 |

| 复杂时序控制 | 弱 | 强(时间轴、串并联、回调) |

| 动态计算 | 弱(需 CSS 变量) | 强(可读取实时数据) |

| 主线程阻塞 | 不受 JS 阻塞影响 | JS 卡顿会拖累动画 |

| 适用 | 状态反馈、循环、入场 | 交互编排、物理引擎、复杂序列 |

经验法则:能用 CSS 就用 CSS,只有需要复杂时序或运行时动态计算时,才引入 JS 动画库。

真实案例:长列表滚动掉帧治理

场景:资讯类 App 的信息流,每个卡片在进入视口时有淡入 + 上移动画。用户反馈快速滑动时明显卡顿,帧率掉到 30fps 以下。

排查:用 Chrome DevTools 的 Performance 面板录制,发现两个问题——(1) 入场动画用了 `top` 而非 `transform`,每帧触发重排;(2) 所有卡片都常驻 `will-change: transform`,导致合成层爆炸、显存吃紧。

修复

cssCode
/* 修复前:top 触发重排 + 全局 will-change */
.card-old {
  position: relative;
  top: 20px;
  will-change: transform, opacity, top; /* ❌ 常驻,太多层 */
  transition: top 0.4s, opacity 0.4s;
}

/* 修复后:transform 只走合成,will-change 仅在动画期间加 */
.card {
  opacity: 0;
  transform: translateY(20px);
}
.card.is-visible {
  opacity: 1;
  transform: translateY(0);
  transition: transform 0.4s ease-out, opacity 0.4s ease-out;
}

数据:帧率从 28fps 回升到稳定 60fps,滚动时的 Long Task 数量减少约 85%,低端安卓机型的卡顿投诉下降约 90%。

常见坑

1.对 display 做过渡无效:display 是离散属性,从 none 到 block 没有中间态。需配合 opacity + visibility,或使用较新的 `@starting-style` 与 `transition-behavior: allow-discrete`。
2.用 left/top 做位移动画卡顿:改用 transform: translate。
3.will-change 滥用:给太多元素长期加 will-change 会耗尽显存,反而变卡。只在动画前加、动画后移除。
4.动画时长一刀切:入场宜 ease-out、退场宜 ease-in,循环旋转用 linear,别全用默认 ease。
5.忽略无障碍:不提供 prefers-reduced-motion 兜底,可能让敏感用户感到不适甚至眩晕。
6.动画抢跑:忘记设 animation-fill-mode: forwards,动画结束后元素"闪回"初始状态。

最佳实践

动画属性优先选 transform 和 opacity,追求 60fps。
时长控制在 200~500ms,符合人眼舒适区间。
合理使用 will-change:动画前开启、结束后置回 auto。
始终提供 prefers-reduced-motion 兜底,保障无障碍。
用 cubic-bezier 微调曲线,让动效有个性而非千篇一律。
动画服务于功能——提供反馈、引导视线,而非炫技。
上线前用 DevTools 的 Performance / Rendering 面板检查是否掉帧。

深入:animation 简写的属性顺序

animation 简写能一行写完八个属性,但顺序有讲究,记错会导致解析歧义。推荐顺序:

cssCode
/* animation: name duration timing-function delay iteration-count direction fill-mode play-state; */
.el {
  animation: slide 0.6s ease-out 0.2s 1 normal forwards running;
}

/* 两个时间值时:第一个是 duration,第二个是 delay */
.el2 {
  animation: pulse 2s 0.5s infinite; /* 时长 2s,延迟 0.5s,无限循环 */
}

/* 多个动画叠加:逗号分隔,各自独立 */
.el3 {
  animation:
    fadeIn 0.4s ease-out,
    slideUp 0.6s ease-out 0.1s,
    glow 2s ease-in-out 0.6s infinite;
}

三条铁律:第一个时间值永远是 duration、第二个才是 delay;animation-name 建议放最前避免和关键字冲突;多动画用逗号分隔,可各自设不同参数。

深入:animation-fill-mode 四态详解

fill-mode 决定动画在「播放前」和「播放后」元素呈现哪一帧,是「动画结束后闪回原状」这个高频 bug 的解药。

| 取值 | 播放前(delay 期间) | 播放后 | 场景 |

| --- | --- | --- | --- |

| none(默认) | 元素原始样式 | 回到原始样式 | 循环动画 |

| forwards | 元素原始样式 | 停在最后一帧 | 入场后保持终态 |

| backwards | 应用第一帧 | 回到原始样式 | 带 delay 的入场,避免闪现 |

| both | 应用第一帧 | 停在最后一帧 | 大多数入场动画的正解 |

cssCode
/* 带延迟的入场:用 backwards/both 让元素在 delay 期间就先隐藏 */
.enter {
  opacity: 1; /* 原始态是可见 */
  animation: fadeInUp 0.5s ease-out 0.3s both;
  /* 若用 none:delay 的 0.3s 里元素先可见,动画一开始又跳回透明,产生闪烁 */
}

@keyframes fadeInUp {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: translateY(0); }
}

记忆法:入场动画几乎总该用 both——它让元素在 delay 期间就锁定第一帧(不闪现),结束后又停在最后一帧(不闪回)。

💻 代码示例:animation-play-state 控制暂停

animation-play-state 能在不重置进度的前提下暂停/恢复动画,常用于「悬停暂停轮播」「聚焦时停止跑马灯」。

cssCode
/* 无缝滚动的 logo 墙,鼠标悬停时暂停 */
.marquee {
  display: flex;
  gap: 40px;
  animation: scroll-x 20s linear infinite;
}
.marquee:hover {
  animation-play-state: paused; /* 悬停暂停,移开继续,不丢进度 */
}

@keyframes scroll-x {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); } /* 配合内容复制一份实现无缝 */
}

💻 代码示例:多关键帧编排复杂动效

用百分比关键帧可以在一条动画里编排「多段」动作,做出「弹入 → 停顿 → 强调 → 收尾」的连续剧情。

cssCode
/* 通知徽章:弹入 + 抖动强调 */
.notify-badge {
  animation: pop-shake 1.2s ease-in-out;
}

@keyframes pop-shake {
  0%   { transform: scale(0) rotate(0); opacity: 0; }
  40%  { transform: scale(1.2) rotate(0); opacity: 1; } /* 过冲弹入 */
  55%  { transform: scale(1) rotate(-12deg); }
  70%  { transform: scale(1) rotate(12deg); }           /* 左右抖动 */
  85%  { transform: scale(1) rotate(-6deg); }
  100% { transform: scale(1) rotate(0); }               /* 归位 */
}

/* 心跳动画:两次快跳 + 一次长停 */
.heartbeat {
  animation: heartbeat 1.4s ease-in-out infinite;
}
@keyframes heartbeat {
  0%   { transform: scale(1); }
  14%  { transform: scale(1.3); }
  28%  { transform: scale(1); }
  42%  { transform: scale(1.3); }
  70%  { transform: scale(1); } /* 后半段停顿,模拟心跳节律 */
}

深入:用 CSS 变量驱动动画

把动画参数抽成 CSS 变量,能实现「一套关键帧、多种表现」,甚至让 JS 只改一个变量就驱动整段动画,兼顾 CSS 的性能与 JS 的灵活。

cssCode
/* 一套关键帧,通过变量控制方向、距离、时长 */
.reveal {
  --distance: 24px;
  --duration: 0.5s;
  animation: reveal var(--duration) ease-out both;
}

@keyframes reveal {
  from {
    opacity: 0;
    transform: translateY(var(--distance));
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* 不同元素复用同一关键帧,只改变量 */
.reveal.from-left  { --distance: -40px; } /* 需配合 translateX 版关键帧 */
.reveal.slow       { --duration: 1s; }

/* 交错入场:用 --i 由内联 style 传入索引,纯 CSS 算延迟 */
.stagger > * {
  animation: reveal 0.5s ease-out both;
  animation-delay: calc(var(--i, 0) * 80ms);
}

配合 JS 只需 element.style.setProperty('--i', index),剩下的插值全交给 GPU,比用 JS 逐帧改 style 高效得多。

真实案例:数字翻牌计数器

运营大屏常见的「数字滚动上涨」效果。纯 CSS 用 counter + @property 可以做出无 JS 的平滑计数动画(@property 让自定义属性可被动画插值)。

cssCode
/* 注册一个可动画的数值型自定义属性 */
@property --num {
  syntax: '<integer>';
  initial-value: 0;
  inherits: false;
}

.counter {
  counter-reset: num var(--num);
  animation: count-up 2s ease-out forwards;
}
.counter::after {
  content: counter(num); /* 把变量渲染成文字 */
}

@keyframes count-up {
  from { --num: 0; }
  to   { --num: 1280; } /* 数字从 0 平滑跳到 1280 */
}

@property 是让「颜色渐变、角度、数值」这类原本无法平滑过渡的自定义属性变得可动画的关键,Chrome 85+、Safari 16.4+ 支持。降级时可用 JS requestAnimationFrame 兜底。

💻 代码示例:conic-gradient 环形进度动画

结合 @property 与锥形渐变,可做出无 SVG、无 JS 的环形进度条:

cssCode
@property --p {
  syntax: '<percentage>';
  initial-value: 0%;
  inherits: false;
}

.ring {
  --p: 0%;
  width: 120px;
  height: 120px;
  border-radius: 50%;
  background: conic-gradient(#007bff var(--p), #eee 0);
  animation: fill 1.5s ease-out forwards;
}

@keyframes fill {
  to { --p: 75%; } /* 环形填充到 75% */
}

交互动效状态机:hover / focus / active 分层

一个高质量按钮的动效往往是多状态叠加的。分清各伪类的语义能避免动效互相打架:

cssCode
.btn {
  transform: translateY(0);
  transition:
    transform 0.15s ease,
    box-shadow 0.15s ease,
    background-color 0.2s ease;
}
.btn:hover  { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,0,0,.15); } /* 悬停浮起 */
.btn:active { transform: translateY(1px);  box-shadow: 0 2px 6px  rgba(0,0,0,.12); } /* 按下回落 */
.btn:focus-visible { outline: 2px solid #007bff; outline-offset: 2px; } /* 键盘聚焦才显焦点环 */
.btn:disabled { opacity: .5; cursor: not-allowed; transform: none; }

用 :focus-visible 而非 :focus,能让鼠标点击时不显示焦点环、键盘 Tab 时才显示,兼顾美观与无障碍。

真实案例:页面切换的共享元素过渡

App 里「列表项点击后放大成详情页」的高级过渡,过去要靠复杂 JS(FLIP 技术)。View Transitions API 让它接近纯声明式。

cssCode
/* 开启视图过渡(需 JS 用 document.startViewTransition 触发切换) */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.4s;
}

/* 给共享元素命名,浏览器自动在新旧页面间做形变过渡 */
.thumbnail {
  view-transition-name: hero-image;
}

某内容 App 接入 View Transitions 后,详情页跳转的「空间连续感」显著提升,用户停留时长环比上升约 12%,而实现代码相比原先手写 FLIP 减少约 200 行。Chrome 111+ 支持同文档过渡,跨文档过渡在更新的版本中逐步落地。

缓动曲线选型速查

不同场景该用什么曲线,这张表能直接照抄:

| 场景 | 推荐曲线 | 理由 |

| --- | --- | --- |

| 元素入场 | ease-out / cubic-bezier(0,0,0.2,1) | 快进慢停,落定感强 |

| 元素退场 | ease-in / cubic-bezier(0.4,0,1,1) | 慢起快出,果断离开 |

| 往返/循环 | ease-in-out | 两端平滑对称 |

| 匀速旋转 | linear | 转速恒定才自然 |

| 弹性/回弹 | cubic-bezier(0.34,1.56,0.64,1) | y 超过 1 产生过冲 |

| 强调/重点 | cubic-bezier(0,0,0.2,1) | 强调曲线,突出主元素 |

| 逐帧/打字机 | steps(n) | 离散跳变而非插值 |

动画调试技巧

Chrome DevTools 提供了专门的动画调试能力,能大幅提升调参效率:

1.Elements 面板旁的 Animations 抽屉:录制并可视化时间轴,可拖动、慢放(0.25x / 0.1x)观察每一帧。
2.Rendering 面板勾选 Paint flashing:重绘区域会闪绿光,一眼看出哪些动画在无谓重绘。
3.Rendering 面板的 Frame Rendering Stats:实时显示 FPS 和 GPU 内存,验证是否掉帧。
4.Layers 面板:查看合成层数量,排查 will-change 滥用导致的「层爆炸」。
cssCode
/* 调试时给动画临时放慢,方便肉眼检查中间帧 */
* { animation-duration: 3s !important; transition-duration: 3s !important; }

💻 代码示例:加载态动效合集

真实项目里最常复用的几种加载动效,一次性备齐:

cssCode
/* 1. 三点跳动(负延迟错相位) */
.loading-dots span {
  display: inline-block;
  width: 8px; height: 8px; border-radius: 50%;
  background: currentColor;
  animation: dot-bounce 1.2s ease-in-out infinite;
}
.loading-dots span:nth-child(1) { animation-delay: -0.32s; }
.loading-dots span:nth-child(2) { animation-delay: -0.16s; }
@keyframes dot-bounce {
  0%, 80%, 100% { transform: scale(0.4); opacity: 0.4; }
  40%           { transform: scale(1);   opacity: 1; }
}

/* 2. 进度条无限来回(不确定进度时用) */
.indeterminate-bar {
  position: relative;
  height: 4px;
  overflow: hidden;
  background: #eee;
}
.indeterminate-bar::before {
  content: '';
  position: absolute;
  inset: 0;
  width: 40%;
  background: #007bff;
  animation: bar-slide 1.2s ease-in-out infinite;
}
@keyframes bar-slide {
  from { transform: translateX(-100%); }
  to   { transform: translateX(350%); }
}

动效设计原则清单

好动效遵循几条可执行的原则,逐条自检即可:

| 原则 | 具体做法 |

| --- | --- |

| 有目的 | 每段动画能说清它在传达什么状态或引导什么 |

| 够快 | 微交互 100-200ms,转场 200-500ms,别超 500ms |

| 可打断 | 用户操作能立即中断/覆盖当前动画,不强迫等待 |

| 一致性 | 全站统一缓动曲线和时长基准,形成动效语言 |

| 高性能 | 只动 transform/opacity,上线前测帧率 |

| 可访问 | prefers-reduced-motion 兜底,焦点用 focus-visible |

总结

| 主题 | 核心要点 |

|---|---|

| Transition | 状态改变触发,只有起终两态,适合简单切换 |

| Animation | @keyframes 定义多帧,可循环往返,适合复杂动效 |

| 缓动函数 | 入场 ease-out、退场 ease-in、匀速 linear、弹性用 cubic-bezier |

| 性能黄金法则 | 只动 transform 和 opacity,避开重排属性 |

| will-change | 精准使用,动画前开、动画后关 |

| 无障碍 | prefers-reduced-motion 必须兜底 |

| 时长基准 | 200~500ms 最自然,<100ms 无感,>500ms 拖沓 |

动画的最高境界是"用户感觉不到动画,只感觉界面很顺"。把性能和无障碍放在第一位,动效才能真正为体验加分。