CSS 探索系列之动画
动画是前端人机交互里重要的一环,合理的动效可以引导用户操作、反馈状态变化、提升页面整体体验。本文聚焦 CSS 体系下两大核心能力:transition(过渡)与 animation(关键帧动画)。
动画基础概念
网页动画原理来自人眼视觉暂留:短时间连续播放多幅静态画面,人眼就会产生物体在运动的错觉。
- 帧(frame):动画中的每一张静态画面。
- 帧率(fps):每秒播放的画面数量。
- 帧时长:单帧画面停留时间,单位 ms。
- 跳帧(掉帧):某一帧处理耗时过长,挤压后续帧,造成动画卡顿。
常见屏幕刷新率为 60Hz,对应 60fps,单帧理想耗时约 16.7ms;也有 90Hz、120Hz 屏幕,单帧更短。动画帧率贴近屏幕刷新率,观感更流畅。
多种实现方案
CSS transition(过渡)
实现两个状态之间的平滑过渡,需要状态变更触发(hover、class 切换、JS 修改样式)。
特点:浏览器接管插值计算,可硬件加速;仅支持起始、结束两个状态,没有中间关键帧。
CSS animation(关键帧动画)
配合 @keyframes 定义多段关键帧,无需外部事件触发就可以自动播放,支持循环、方向控制、停留状态,CSS 原生能力最强。
Canvas 动画
通过 Canvas API 在画布上不断清除、重绘图形。
适用:大量粒子、复杂游戏画面;完全由 JS 控制渲染逻辑。
SVG 原生动画
<animate>、<animateTransform> 等标签,直接在 SVG 内部定义属性动画。
优势:矢量无损。
缺点:复杂场景性能较差,只能作用于 SVG 内部元素。
定时器动画
通过 setInterval、setTimeout 循环修改 DOM 样式实现动画。
原理:定时回调不断更新元素样式。
缺点:容易频繁触发重排、重绘,移动端容易卡顿;定时器时间与浏览器刷新时钟不同步,会出现跳帧。
注意:习惯写 16ms 间隔,目标贴近 60fps,但不能和浏览器刷新节拍对齐,不是最优方案。
requestAnimationFrame
浏览器专门为动画提供的 API,回调执行时机和屏幕刷新节拍对齐。
注意:相比定时器,避免时钟不同步问题,性能更优;依然需要 JS 手动更新每帧样式。
选型建议
- 简单状态变化:优先
transition - 自动播放、循环、多关键帧:优先
animation - 大量粒子、游戏:Canvas
- SVG 图形动效:SVG 原生动画
- JS 精细控制每帧逻辑:
requestAnimationFrame
CSS transition 过渡
transition 实现隐式过渡:只定义初始状态和结束状态,中间所有插值由浏览器自动计算,必须依靠状态变更来触发(hover、增加 class、JS 改样式)。
<style>
.transition1,
.transition2 {
display: inline-block;
width: 100px;
height: 100px;
background-color: #0aa;
border-radius: 20px;
}
.transition1:hover,
.transition2:hover {
width: 200px;
height: 200px;
background-color: orange;
transform: rotate(180deg);
border-radius: 100px;
}
.transition2 {
transition: width 2s, height 2s, background-color 2s, transform 2s, border-radius 2s;
}
</style>
<section>
<div class="transition1"></div>
<div class="transition2"></div>
</section>
transition 子属性
transition: <property> <duration> <timing-function> <delay>;
支持多属性逗号分隔:
transition: width 2s, height 2s, transform 2s;
transition-property:指定需要过渡的 CSS 属性;未声明的属性会直接瞬间变化,不参与过渡。transition-duration:过渡持续时间,单位s、ms,默认0s;不设非 0 时长就看不到过渡。transition-timing-function缓动函数,控制动画进度曲线。ease:先快后慢(默认)ease-in:由慢到快(加速)ease-out:由快到慢(减速)ease-in-out:先加速后减速linear:匀速cubic-bezier(x1,y1,x2,y2):自定义贝塞尔曲线steps(n, pos):阶梯式分段动画;step-start、step-end为简写。
transition-delay:过渡开始前的等待时间;支持负值,负值会直接跳过部分起始阶段。
/* steps(4,end):切4段,共发生4次跳跃;默认position=end可以省略 */
.step-end {
transition: transform 2s steps(4);
}
/* steps(4,start):一开始就跳第一阶 */
.step-start {
transition: transform 2s steps(4, start);
}
属性列表长度匹配规则:如果各个子属性逗号分隔的列表长度不一致,会以 transition-property 的长度为准,短的列表循环复用;超出长度的项直接截断。
div {
/* 要过渡3个属性 */
transition-property: opacity, left, top;
/* duration只写2个时长 */
transition-duration: 3s, 5s;
}
transition 事件
现代浏览器一共提供 4 个 transition 事件,均为 TransitionEvent 接口:
transitionrun:过渡任务被创建,延迟阶段一开始就触发,即便还处在transition-delay等待期。transitionstart:延迟结束,属性插值变化正式开始时触发;无延迟、负延迟时,与transitionrun几乎同时触发。transitionend:过渡完整执行完毕触发;如果中途被打断,则不会触发。transitioncancel:过渡被中断、取消时触发;一旦触发该事件,就不会再执行transitionend。
每一个参与过渡的 CSS 属性,分别触发一套完整事件;通过事件对象 event.propertyName 判断是哪个属性:
elem.addEventListener('transitionend', (e) => {
console.log('过渡结束:', e.propertyName);
});
elem.addEventListener('transitioncancel', (e) => {
console.log('过渡被中断:', e.propertyName);
});
注意:如果在过渡还没结束时,属性被再次修改,或者元素变成 display: none,transitionend 将不会触发。
transition 的局限
- 必须依靠状态事件触发,页面加载不能自动播放。
- 只有起始、结束两个状态,无法定义中间关键帧。
- 属于一次性动画,想要重复需要再次触发状态变更。
- 直接
appendChild新增元素、或者把display: none切回可见,不会产生过渡;DOM 直接落在结束状态。
解决方法:DOM 插入后,使用短暂 setTimeout 延迟再修改样式,浏览器可以捕获到状态差异。
CSS animation 关键帧动画
animation 搭配 @keyframes 关键帧规则,可以定义一套完整动画序列,支持自动播放、循环、反向、结束停留,不需要事件触发。
<style>
.animation {
width: 50px;
height: 50px;
margin-bottom: 10px;
background-color: #0aa;
border-radius: 25px;
animation: move2 2s ease 1s infinite;
}
@keyframes move2 {
0% {
transform: translate(0, 0);
background-color: red;
}
100% {
transform: translate(552px, 0);
background-color: orange;
}
}
</style>
<section>
<div class="animation"></div>
</section>
animation 子属性
简写完整默认:
animation: none 0s ease 0s 1 normal none running;
animation-name:@keyframes关键帧的名称animation-duration:单次动画周期时长,默认0s;不设非 0 时长就看不到动画animation-timing-function:关键帧之间的缓动函数,用法同 transitionanimation-delay:动画开始前延迟时间animation-iteration-count:循环次数。数字、infinite(无限循环),支持小数animation-direction:播放方向。normal正向;reverse反向;alternate来回交替;alternate-reverse先反向再交替animation-fill-mode:动画前后如何应用样式。none默认;forwards结束后停留在最后一帧;backwards在 delay 阶段应用起始关键帧(reverse时应用的是结束关键帧);both同时开启forwards与backwardsanimation-play-state:running运行、paused暂停;可用 JS 动态控制启停
@keyframes 关键帧
使用百分比标记时间点:
0%等价别名from,动画起始;100%等价别名to,动画结束;- 中间可以写任意百分比节点,定义中间状态;
- 如果省略
0%、100%,浏览器取元素当前计算样式作为起始、结束。
动画事件
animationstart:动画开始animationiteration:每一次新循环开始animationend:动画完整结束animationcancel:动画被中断、取消时触发;一旦触发,就不会再执行animationend
elem.addEventListener('animationstart', listener);
elem.addEventListener('animationiteration', listener);
elem.addEventListener('animationend', listener);
elem.addEventListener('animationcancel', listener);
animation 优缺点
优点:
- 无需 JS 触发,可自动播放;
- 支持多关键帧、循环、反向、状态停留;
- 浏览器做优化,后台标签页会降频。
缺点:
- 运行时难以 JS 动态修改关键帧内容;
- 精细的时间轴控制能力弱于 JS 动画库。
重启动画小技巧
CSS 本身没有直接重置动画语法,常见方法:
- JS 移除动画 class,短暂延迟后重新加上;
- 克隆替换 DOM 节点;
- 修改
animation-name切换关键帧名称。
主线程与合成线程
浏览器渲染分为两大线程:
- 主线程:执行 JS、CSS 样式计算、布局(重排 reflow)、绘制(重绘 repaint),生成图层位图。
- 合成线程(Compositor):交给 GPU,只做图层的位移、缩放、旋转、透明度,不触发重排重绘,性能极高。
性能等级:
- 合成线程动画(最优):
transform、opacity;不会触发重排重绘,GPU 加速。 - 重绘:
background-color、box-shadow;触发绘制,不触发布局。 - 重排(最差):
width、height、top、margin、line-height;每一帧都会重新布局,开销巨大。
开发最佳实践:优先使用 transform + opacity 做动画,尽量避免动画过程修改会触发重排的属性。
备注:transform: translateZ(0)、translate3d(0,0,0) 可以强制提升到独立合成图层,但不要滥用,图层过多会消耗大量显存。
可动画属性
并不是所有 CSS 属性都支持插值动画,MDN 有完整「动画性 CSS 属性」列表。
能否做过渡、动画,由该属性的「动画类型(animation-type)」决定:
- 可插值(interpolable):可以计算中间值,实现平滑渐变,
transition、animation直接生效。 - 离散(discrete):没有中间插值,值在进度约 50% 时直接跳转,不是平滑过渡。
可插值属性分类(平滑动画):
变换类(性能最优,推荐优先使用):
transform、rotate、scale、translate、transform-origin透明度、滤镜:
opacity、filter、backdrop-filter颜色类:
color、background-color、border-color、outline-color、box-shadow、text-shadow(阴影整体插值)尺寸、间距(会触发重排,性能较差):
width、height、min-width、max-width、min-height、max-height、margin-*、padding-*、border-radius、border-width位置(重排,不推荐做动画):
top、left、right、bottom字体文本:
font-size、font-weight、font-stretch、font-variation-settings、letter-spacing、word-spacing、line-height、vertical-align背景:
background-position、background-size其他:
z-index(整数插值)、clip-path、flex、flex-grow、flex-shrink、mask相关属性all关键字:一次性指定全部可动画属性;不推荐生产使用,大量属性加入过渡,容易引发性能问题。
离散属性(无平滑插值,默认不能做平滑过渡):
displaypositioncontentfont-familyoverflowpointer-eventsvisibility(特殊:visibility: hidden、visible有独立插值规则,可见状态会尽量保持到动画后段再跳转)新特性:
transition-behavior: allow-discrete可以让离散属性参与过渡流程;不会产生平滑渐变,只是把值切换纳入过渡的开始、结束时机,而不是默认的进度 50% 跳变。
不要对 auto 值做过渡、动画:auto 是运行时布局计算值,规范没有定义插值规则,浏览器行为不一致,效果不可预期。
transition 和 animation 核心对比
| 对比项 | transition | animation |
|---|---|---|
| 触发方式 | 需要状态变更触发(hover、class、JS) | 可自动播放,无需外部触发 |
| 关键帧 | 只有起始、结束两个状态 | 支持 @keyframes 多关键帧 |
| 循环 | 不能原生循环,需要重复触发 | 支持 infinite 无限循环 |
| 结束状态 | 触发态还在时停在结束值;触发态取消后过渡回原样式 | 默认结束后回到动画前样式;fill-mode 可保持结束帧 |
周边工具库
- Animate.css:现成预设 CSS 动画类库,直接加 class 使用;v4 版本带
animate__前缀,支持 CSS 变量自定义时长延迟;注意做好prefers-reduced-motion无障碍兼容。 - Anime.js:JS 动画库,可以操作 CSS、SVG、DOM 对象,强大时间轴、交错、缓动能力。
- Lottie-web:解析 AE 导出的 JSON 动效,设计师产出 AE 动画,前端直接渲染,适合复杂业务动效。
- Three.js:WebGL 3D 渲染,三维场景动画。