技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTBR

讲解 CSS 过渡和动画:transition 与 animation

所属馆藏
Browser
文件格式
Markdown
原始路径
Browser/37-讲解 CSS 过渡和动画 transition animation
本文目录8 个章节
  1. 一、帧、刷新率和卡顿
  2. 二、JavaScript 动画与 CSS 动画
  3. 三、学习 CSS Transition
  4. 四、学习 CSS Animation
  5. 五、现代 CSS 动画能力
  6. 六、动画性能建议
  7. 七、总结
  8. 参考资料

讲解 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-durationtransition-timing-functiontransition-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. 离散属性、进入和退出动画

现代浏览器可以对部分离散属性做“在某个时间点切换”的过渡,而不是生成连续中间值。displaynone 进入可见状态时,浏览器需要让元素在动画期间参与渲染;退出时则通常在动画结束后切换为 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-styleallow-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-directionnormalreversealternatealternate-reverse
  • animation-fill-modenoneforwardsbackwardsboth
  • animation-play-staterunningpaused
  • 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-functionsteps()

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-startjump-endjump-nonejump-bothstartend 对阶跃发生点有不同定义。不要仅凭“n 就是画面帧数”推断精灵图的实际帧数,还要结合图片切片和 background-position 的范围。

steps 阶跃函数示意

steps 动画时间线示意

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 还提供 animationNamepseudoElementelapsedTime。元素从 DOM 移除或动画样式被取消时,不应等待 animationend 做必须执行的清理;需要可靠控制时可以使用 WAAPI 的 finishedcancelcommitStyles 等能力。

五、现代 CSS 动画能力

1. 动画合成

animation-composition 可以指定多个动画同时修改同一属性时如何合成,例如替换、相加或累积。它适合组件动画组合,但目标浏览器支持情况和属性的可组合类型需要验证。

2. 滚动驱动动画

animation-timelineanimation-rangescroll()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;
  }
}

对于需要保持信息可理解的动画,不要只把时长改成极小值,也可以直接关闭装饰动画并保留状态变化。

六、动画性能建议

  1. 优先测量,不要看到 transform 就假设一定更快;
  2. transformopacity 在合适条件下容易由合成线程处理;
  3. 修改 topleft、宽高、字体等属性可能触发布局和绘制;
  4. 大面积阴影、滤镜、混合、裁剪和高分辨率图片可能使 Paint/Raster 成为瓶颈;
  5. 不要给所有元素设置 will-changetranslateZ(0),合成层会消耗内存;
  6. 使用 Performance 面板观察 Long Task、Recalculate Style、Layout、Paint、Raster 和 Composite;
  7. 输入驱动的 JavaScript 动画使用 requestAnimationFrame 批量写入,读取布局信息要和写入分离;
  8. 页面真实体验应结合 INP、LCP、CLS、设备刷新率和 prefers-reduced-motion 评估。

七、总结

  • Transition 适合两个计算样式状态之间的过渡,Animation 适合关键帧、循环、方向和时间线控制;
  • 不是所有属性都可连续插值,现代 CSS 正逐步支持 display 等离散过渡;
  • animation-delay 的负值表示从时间线中途开始,不是普通的延迟;
  • transitionend/animationend 在取消、隐藏或移除节点时可能不触发;
  • WAAPI、滚动驱动动画、animation-composition@starting-style 扩展了 CSS 动画能力;
  • 先考虑用户的运动偏好,再用浏览器性能工具验证实际渲染路径。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS