讲解 CSS 过渡和动画:transition 与 animation
Category(分类): Browser, CSS Status: 已更新
Web 动画是元素的可动画属性随时间变化,并在浏览器渲染机会中产生连续视觉结果。它不一定每次都能按屏幕刷新率生成新帧:主线程长任务、布局和绘制成本、设备刷新率、后台页面节流和系统负载都会影响最终体验。
一、帧、刷新率和卡顿
屏幕刷新率以 Hz 表示。60Hz 设备的理想帧间隔约为 16.67ms,90Hz 约为 11.11ms,120Hz 约为 8.33ms。这只是显示设备的时间间隔,不是浏览器保证的 JavaScript 执行预算。
当浏览器错过某个显示机会时,下一帧可能继续显示旧画面,用户就会看到卡顿、跳跃或输入延迟。动画性能不应只看 FPS,还要关注长任务、输入延迟、掉帧和实际设备。
setTimeout() 和 setInterval() 的回调会进入任务队列,不能保证精确执行时间,也不会自动与绘制同步。requestAnimationFrame() 会请求在下一次适合绘制的时机调用,适合需要 JavaScript 逐帧更新视觉状态的场景,但在后台页面或节能模式下仍可能被暂停或降频。
二、JavaScript 动画与 CSS 动画
常见方式有:
1. JavaScript + requestAnimationFrame
const ball = document.querySelector('.ball')
let startTime = null
function frame(timestamp) {
if (startTime === null) startTime = timestamp
const progress = Math.min((timestamp - startTime) / 800, 1)
ball?.style.setProperty('transform', `translateX(${progress * 240}px)`)
if (progress < 1) requestAnimationFrame(frame)
}
requestAnimationFrame(frame)
适合动画逻辑依赖实时数据、物理模拟或复杂交互的场景。要避免在每一帧交错读取布局和写入样式,否则可能造成布局抖动。
2. Web Animations API
Web Animations API(WAAPI)提供 JavaScript 操作动画时间线、播放状态和关键帧的标准接口,也是 CSS Animation/Transition 与脚本动画之间的桥梁:
const element = document.querySelector('.box')
const animation = element?.animate([
{ transform: 'translateX(0)', opacity: 0.4 },
{ transform: 'translateX(240px)', opacity: 1 }
], {
duration: 600,
easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)',
fill: 'both'
})
animation?.finished.then(() => {
console.log('动画完成')
})
3. CSS Transition 和 CSS Animation
CSS 动画把状态和时间线交给浏览器,通常更容易声明和维护。CSS Transition 适合两个状态之间的变化;CSS Animation 通过 @keyframes 自定义一个或多个关键帧,并支持循环、方向、暂停和填充模式。
三、学习 CSS Transition
1. 过渡的本质
Transition 在某个 CSS 属性的计算值发生变化时,根据变化前后的值计算中间状态。它不是必须写出两个显式关键帧,也不是所有 CSS 属性都能平滑插值。
.button {
color: white;
background: #2563eb;
transition: background-color 180ms ease, transform 180ms ease;
}
.button:hover,
.button:focus-visible {
background: #1d4ed8;
transform: translateY(-2px);
}
初次把元素插入 DOM 或首次应用样式时,通常没有“变化前的已渲染状态”,因此普通 Transition 不一定触发。现代 CSS 可以使用 @starting-style 为进入状态提供起始值;浏览器支持情况应结合目标环境验证。
属性是否可过渡取决于它的动画类型和具体值。颜色、长度、角度、透明度和许多变换可以插值;display 等离散属性历史上不能平滑插值,但现代浏览器正在通过 transition-behavior: allow-discrete 支持部分离散过渡。
不要把 height: auto 的过渡当作跨浏览器稳定能力。可以使用显式高度、网格/裁剪技巧、interpolate-size(视支持情况)或 WAAPI,并根据目标浏览器测试。
2. Transition 语法
.panel {
transition: <property> <duration> <timing-function> <delay>;
}
实际写法通常是:
.panel {
transition-property: opacity, transform;
transition-duration: 200ms, 300ms;
transition-timing-function: ease-out, cubic-bezier(0.2, 0.8, 0.2, 1);
transition-delay: 0ms, 50ms;
}
简写中的时间值按出现顺序解析:第一个时间通常是 duration,第二个是 delay:
.panel {
transition: opacity 200ms ease-out 50ms;
}
如果 transition-duration、transition-timing-function 或 transition-delay 的列表比 transition-property 短,CSS 会重复列表中的值以匹配;过长的列表会被截断。
.card {
transition-property: opacity, transform, color;
transition-duration: 200ms, 400ms;
/* 等价于 200ms, 400ms, 200ms */
}
原文中的 transition: opacity, 3s 容易被误写成属性和时长之间用逗号。推荐写成:
.card {
transition: opacity 3s ease;
}
3. 缓动函数
常用缓动函数包括:
linear:匀速变化;ease:默认的先快后慢组合;ease-in:开始较慢;ease-out:结束较慢;ease-in-out:两端较慢;cubic-bezier(x1, y1, x2, y2):自定义三次贝塞尔曲线;steps(n, <jump-term>):离散阶跃变化。
CSS 还支持更丰富的线性缓动表达式,具体以目标浏览器和规范支持为准。
4. 离散属性、进入和退出动画
现代浏览器可以对部分离散属性做“在某个时间点切换”的过渡,而不是生成连续中间值。display 从 none 进入可见状态时,浏览器需要让元素在动画期间参与渲染;退出时则通常在动画结束后切换为 none。
.dialog {
display: none;
opacity: 0;
transition:
opacity 200ms ease,
display 200ms allow-discrete;
}
.dialog.is-open {
display: block;
opacity: 1;
}
@starting-style {
.dialog.is-open {
opacity: 0;
}
}
@starting-style 和 allow-discrete 的支持度、实现细节和可访问性行为需要测试。隐藏对话框时还要处理焦点、aria-hidden、键盘操作和屏幕阅读器语义,不能只依靠透明度。
5. Transition 事件
Transition 相关事件包括:
transitionrun:过渡创建,在 delay 之前触发;transitionstart:实际过渡开始,在 delay 之后触发;transitionend:过渡正常结束;transitioncancel:过渡被取消。
const box = document.querySelector('.box')
box?.addEventListener('transitionrun', event => {
console.log('过渡已创建', event.propertyName)
})
box?.addEventListener('transitionstart', event => {
console.log('过渡开始', event.elapsedTime)
})
box?.addEventListener('transitionend', event => {
console.log('过渡结束', event.propertyName)
})
box?.addEventListener('transitioncancel', () => {
console.log('过渡取消')
})
TransitionEvent.elapsedTime 以秒为单位,通常不包含 delay。若元素被设为 display: none、属性值再次改变或节点被移除,过渡可能被取消,此时不能依赖 transitionend 一定触发。
四、学习 CSS Animation
1. @keyframes
CSS Animation 通过带名称的 @keyframes 规则声明关键帧:
@keyframes slide-in {
from {
opacity: 0;
transform: translateX(-24px);
}
50% {
opacity: 0.8;
}
to {
opacity: 1;
transform: translateX(0);
}
}
.message {
animation: slide-in 600ms ease-out both;
}
from 等价于 0%,to 等价于 100%。同一 @keyframes 中同名属性遵循规则内的级联;多个同名 @keyframes 规则通常以后定义的规则为准。关键帧缺失的属性会从可用的起始/结束计算值或邻近关键帧补齐,具体插值取决于属性类型。
2. animation-* 属性
animation 是以下属性的简写:
animation-name:匹配@keyframes名称;animation-duration:一个周期的时长,默认0s;animation-timing-function:每个关键帧区间的缓动;animation-delay:开始前等待时间,可以为负值;animation-iteration-count:播放次数或infinite;animation-direction:normal、reverse、alternate、alternate-reverse;animation-fill-mode:none、forwards、backwards、both;animation-play-state:running或paused;animation-timeline:使用的动画时间线,支持度依环境而定。
.notice {
animation-name: slide-in;
animation-duration: 600ms;
animation-timing-function: ease-out;
animation-delay: 100ms;
animation-iteration-count: 2;
animation-direction: alternate;
animation-fill-mode: both;
animation-play-state: running;
}
简写中第一个时间值是 duration,第二个时间值是 delay,名称通常放在末尾以避免歧义:
.notice {
animation: 3s ease-in 1s 2 alternate both running slide-in;
}
负 delay 的含义是动画开始时已经经过了一段时间:
.notice {
animation: slide-in 2s linear -500ms both;
}
这不是“延迟 500ms 后开始”,而是从动画时间线的中途开始。animation-duration: 0s 可能触发动画事件,但不会产生可见的时间变化。
3. animation-timing-function 和 steps()
cubic-bezier() 适合连续补间;steps(n, jump-term) 把时间切成 n 个阶跃区间,适合精灵图、数字跳变和逐帧效果。
.sprite {
animation: sprite 800ms steps(8, end) infinite;
}
@keyframes sprite {
from {
background-position-x: 0;
}
to {
background-position-x: -800px;
}
}
jump-start、jump-end、jump-none、jump-both、start 和 end 对阶跃发生点有不同定义。不要仅凭“n 就是画面帧数”推断精灵图的实际帧数,还要结合图片切片和 background-position 的范围。
4. Animation 事件
animationstart:动画开始;负 delay 会反映为非零的elapsedTime;animationiteration:一个周期结束并开始下一个周期;animationend:动画正常结束;animationcancel:动画被移除、节点隐藏或其他原因取消。
const target = document.querySelector('.notice')
target?.addEventListener('animationstart', event => {
console.log(event.animationName, event.elapsedTime)
})
target?.addEventListener('animationiteration', event => {
console.log('进入下一周期', event.animationName)
})
target?.addEventListener('animationend', event => {
console.log('动画结束', event.animationName)
})
target?.addEventListener('animationcancel', event => {
console.log('动画取消', event.animationName)
})
AnimationEvent 还提供 animationName、pseudoElement 和 elapsedTime。元素从 DOM 移除或动画样式被取消时,不应等待 animationend 做必须执行的清理;需要可靠控制时可以使用 WAAPI 的 finished、cancel 和 commitStyles 等能力。
五、现代 CSS 动画能力
1. 动画合成
animation-composition 可以指定多个动画同时修改同一属性时如何合成,例如替换、相加或累积。它适合组件动画组合,但目标浏览器支持情况和属性的可组合类型需要验证。
2. 滚动驱动动画
animation-timeline、animation-range、scroll() 和 view() 可以让动画进度跟随滚动时间线,而不是固定的文档时间线:
.progress {
animation-name: grow;
animation-duration: auto;
animation-timing-function: linear;
animation-fill-mode: both;
animation-timeline: scroll(root block);
animation-range: 0% 100%;
}
@keyframes grow {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
当使用 animation 简写后再设置 animation-timeline,要注意简写可能把未写出的 animation-timeline 重置为 auto。可以把时间线属性放在简写之后,或使用 @supports 按能力渐进增强。
3. 可访问性和减少动画
闪烁、快速缩放和大范围移动可能诱发眩晕、偏头痛或光敏反应。应尊重用户的 prefers-reduced-motion 设置,并为关键动画提供暂停、跳过或无动画替代:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 1ms !important;
animation-iteration-count: 1 !important;
transition-duration: 1ms !important;
scroll-behavior: auto !important;
}
}
对于需要保持信息可理解的动画,不要只把时长改成极小值,也可以直接关闭装饰动画并保留状态变化。
六、动画性能建议
- 优先测量,不要看到
transform就假设一定更快; transform和opacity在合适条件下容易由合成线程处理;- 修改
top、left、宽高、字体等属性可能触发布局和绘制; - 大面积阴影、滤镜、混合、裁剪和高分辨率图片可能使 Paint/Raster 成为瓶颈;
- 不要给所有元素设置
will-change或translateZ(0),合成层会消耗内存; - 使用 Performance 面板观察 Long Task、Recalculate Style、Layout、Paint、Raster 和 Composite;
- 输入驱动的 JavaScript 动画使用
requestAnimationFrame批量写入,读取布局信息要和写入分离; - 页面真实体验应结合 INP、LCP、CLS、设备刷新率和
prefers-reduced-motion评估。
七、总结
- Transition 适合两个计算样式状态之间的过渡,Animation 适合关键帧、循环、方向和时间线控制;
- 不是所有属性都可连续插值,现代 CSS 正逐步支持
display等离散过渡; animation-delay的负值表示从时间线中途开始,不是普通的延迟;transitionend/animationend在取消、隐藏或移除节点时可能不触发;- WAAPI、滚动驱动动画、
animation-composition和@starting-style扩展了 CSS 动画能力; - 先考虑用户的运动偏好,再用浏览器性能工具验证实际渲染路径。