CSS 动画与过渡效果
CSS 动画与过渡效果
CSS 动画和过渡为网页增添交互性和视觉吸引力,是提升用户体验、传达界面状态的关键手段。一个恰到好处的按钮反馈、一段流畅的页面入场,往往比十行文案更能让用户感受到产品的"质感"。
为什么需要动画
界面动画不只是"好看",它承担着实实在在的功能:
据 Google Material Design 团队的研究,界面过渡时长控制在 200~500ms 之间时,用户感知最自然;低于 100ms 用户几乎察觉不到,高于 500ms 则开始感到拖沓。
过渡(Transition)与动画(Animation)的区别
这是初学者最容易混淆的一对概念:
| 维度 | Transition(过渡) | Animation(动画) |
|---|---|---|
| 触发方式 | 需状态改变触发(如 :hover) | 可自动播放,无需触发 |
| 关键帧 | 只有起点和终点两个状态 | 可定义任意多个中间关键帧 |
| 循环 | 不能循环 | 可无限循环 |
| 方向控制 | 不支持往返 | 支持 alternate 往返 |
| 适用场景 | 简单的状态切换 | 复杂、连续、循环的动效 |
一句话总结:过渡是"从 A 到 B",动画是"A→B→C→D 甚至循环往复"。
过渡效果(Transition)
基本属性:
时间函数(缓动曲线):
可过渡的属性与不可过渡的属性:
💻 代码示例:按钮过渡效果
.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` 性能更好,因为浏览器无需监听所有属性。
.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)
基本属性:
@keyframes 规则:
常用控制关键字:
💻 代码示例:加载动画
/* 旋转加载器: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);
}
}💻 代码示例:骨架屏与打字机效果
/* 骨架屏微光扫过效果:内容加载前的占位动画 */
.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
/* 元素从下方淡入,结束后停留在最终状态 */
.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; }动画性能:重排、重绘与合成
理解浏览器的渲染流水线,是写出流畅动画的核心。浏览器渲染大致分三步:
结论:动画尽量只改 transform 和 opacity。 它们能被提升到独立的合成层,交由 GPU 处理,轻松跑满 60fps。
| 动画属性 | 触发阶段 | 性能 | 建议 |
|---|---|---|---|
| transform | 仅合成 | 极佳 | 首选(位移/缩放/旋转) |
| opacity | 仅合成 | 极佳 | 首选(淡入淡出) |
| filter | 重绘+合成 | 良好 | 谨慎使用 |
| box-shadow | 重绘 | 一般 | 用 transform 阴影层替代 |
| width/height | 重排 | 差 | 改用 transform: scale |
| top/left | 重排 | 差 | 改用 transform: translate |
💻 代码示例:动画性能优化
/* 使用 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 应予以尊重。
/* 尊重系统级"减少动态效果"设置 */
@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,避免因宽度动画触发重排。
.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 时,就会产生"过冲回弹"的弹性效果。
/* 常见缓动曲线的贝塞尔等价写法 */
.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 逐格移动。
/* 精灵图逐帧动画:一张 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 是一个巧妙技巧:让动画像"已经播放了一段时间"一样立即从中途开始,常用于让多个循环动画错开相位。
/* 三个圆点的加载动画,用负延迟错开相位形成跳动波浪 */
.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 效果,且同样运行在合成层,性能优异。
/* 翻牌效果:鼠标悬停时卡片沿 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 的依赖:
/* 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`,导致合成层爆炸、显存吃紧。
修复:
/* 修复前: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%。
常见坑
最佳实践
深入:animation 简写的属性顺序
animation 简写能一行写完八个属性,但顺序有讲究,记错会导致解析歧义。推荐顺序:
/* 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 | 应用第一帧 | 停在最后一帧 | 大多数入场动画的正解 |
/* 带延迟的入场:用 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 能在不重置进度的前提下暂停/恢复动画,常用于「悬停暂停轮播」「聚焦时停止跑马灯」。
/* 无缝滚动的 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%); } /* 配合内容复制一份实现无缝 */
}💻 代码示例:多关键帧编排复杂动效
用百分比关键帧可以在一条动画里编排「多段」动作,做出「弹入 → 停顿 → 强调 → 收尾」的连续剧情。
/* 通知徽章:弹入 + 抖动强调 */
.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 的灵活。
/* 一套关键帧,通过变量控制方向、距离、时长 */
.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 让自定义属性可被动画插值)。
/* 注册一个可动画的数值型自定义属性 */
@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 的环形进度条:
@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 分层
一个高质量按钮的动效往往是多状态叠加的。分清各伪类的语义能避免动效互相打架:
.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 让它接近纯声明式。
/* 开启视图过渡(需 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 提供了专门的动画调试能力,能大幅提升调参效率:
/* 调试时给动画临时放慢,方便肉眼检查中间帧 */
* { animation-duration: 3s !important; transition-duration: 3s !important; }💻 代码示例:加载态动效合集
真实项目里最常复用的几种加载动效,一次性备齐:
/* 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 拖沓 |
动画的最高境界是"用户感觉不到动画,只感觉界面很顺"。把性能和无障碍放在第一位,动效才能真正为体验加分。