重学 Web 动画(一):CSS 动画基础

2026.01.02 · 2465字 · 9分钟阅读

前阵子写几个 hover 动画时,发现常用的 CSS 属性都有点生疏了。趁这次把 opacitytransformtransitionanimation@keyframesclip-path 这几个最常打交道的捋一遍,当备忘。

🌟 一句话前置:动画首选 opacitytransform,它们只触发合成(Composite),不会触发重排和重绘,性能最好;其他属性能不动就别动。

opacity

透明度,0 ~ 1。看起来只是个数值,但配合 transition 就能做出很自然的淡入淡出效果:

css
.fade-card {
  opacity: 0.4;
  transition: opacity 0.4s ease;
}
.fade-card:hover {
  opacity: 1;
}

也可以配合 @keyframes 做循环呼吸效果,常见于通知红点、加载提示:

css
.dot {
  animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.3; }
}

⚠️ 小坑opacity: 0 的元素仍然可点击仍会被屏幕阅读器读到。要彻底隐藏要配合 visibility: hiddenpointer-events: nonearia-hidden

transform

让元素移动、旋转、缩放、倾斜,全都走 GPU。

translate - 位移

css
transform: translateX(100px);
transform: translateY(50px);
transform: translate(100px, 50px);
transform: translate3d(100px, 50px, 0);  /* 强制 GPU 加速 */

要移动元素就用 translate,不要改 left/top —— 后者会触发重排。

rotate - 旋转

css
transform: rotate(45deg);      /* 顺时针 */
transform: rotate(-45deg);     /* 逆时针 */
transform: rotateX(45deg);     /* 沿 X 轴翻转,有 3D 效果 */
transform: rotateY(45deg);

scale - 缩放

css
transform: scale(1.5);
transform: scale(0.5);
transform: scaleX(2);

按钮的点按反馈也是 scale 的常见用法 —— hover 时变色提示可点,按下时缩一点给出物理反馈:

css
.card:hover  { background: #2a2a2a; }
.card:active { transform: scale(0.96); }

组合 & 顺序

多个变换写在一起,顺序会影响结果:

css
transform: translateX(100px) rotate(45deg);  /* 先平移,再就地旋转 */
transform: rotate(45deg) translateX(100px);  /* 先旋转坐标系,再平移,会画出弧 */

transform-origin

默认以元素中心为原点。要换原点用 transform-origin

css
transform-origin: top left;    /* 左上角 */
transform-origin: center;      /* 中心,默认值 */
transform-origin: 50% 100%;    /* 底部中心 */

transition

transition 是最简单的动画方式 —— 属性值一变,它就自动补出过渡。完整语法:transition: property duration timing-function delay;

css
.button {
  background: #333;
  transition: background 0.3s ease;
}

.button:hover {
  background: #666;
}

四个子属性

  • transition-property:要过渡的属性,all 表示全部
  • transition-duration:时长,如 0.3s300ms
  • transition-timing-function:缓动函数,决定动画的「感觉」
  • transition-delay:延迟

timing-function 缓动函数

内置的几个:

  • linear:匀速,比较生硬
  • ease:默认值,先快后慢
  • ease-in:慢进
  • ease-out:慢出,最自然
  • ease-in-out:慢进慢出

怎么选

  • 进入ease-out(弹窗、菜单展开 —— 元素从无到有)
  • 离开ease-in(关闭、隐藏 —— 元素从有到无)
  • 双向ease-in-out(来回切换的状态)
  • UI 动画一般避开 linear,匀速会让人觉得机械、不自然

不够用时上 cubic-bezier() 自定义曲线。配 easings.net 直接抄就行。

css
/* 弹性效果 */
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);

animation

要做循环、多阶段动画,transition 就不够了,得用 animation。完整语法:animation: name duration timing-function delay iteration-count direction fill-mode;

下面几个常见的小动画,分别对应 animation 几种典型的用法:

通知红点呼吸 — 多属性同时变化(scale + opacity),适合做消息提醒、在线状态点。

进度条滑动 — 单属性循环位移,适合做不确定进度的加载提示。

心跳节奏 — 多阶段 keyframes(百分比写法),能做出"咚-哒"两段式的节奏感 —— 这是 transition 表达不出来的。

这三个例子覆盖了 animation 大部分日常用法 —— 单属性循环、多属性组合、多阶段节奏。下面拆开看每个子属性。

常用子属性

  • animation-name:对应 @keyframes 的名字
  • animation-duration:时长
  • animation-timing-function:缓动函数,和 transition 一样
  • animation-delay:延迟
  • animation-iteration-count:播放次数,infinite 即无限循环
  • animation-direction:播放方向
    • normal / reverse / alternate(来回播放,呼吸灯效果常用)
  • animation-fill-mode:动画结束后停在哪一帧
    • forwards 停在最后一帧(最常用,避免动画播完跳回原位)
    • backwards 停在第一帧 / both 两者都应用
  • animation-play-staterunning / paused,用来暂停

简写小技巧

时长建议单独写 animation-duration,其他用简写。原因是简写里 duration 和 delay 顺序最容易写反,单独拎出来不会错。

css
.loading {
  animation: spin linear infinite;
  animation-duration: 1s;
}

@keyframes

两种写法。简单两帧用 from / to

css
@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

多阶段用百分比:

css
@keyframes bounce {
  0%   { transform: translateY(0); }
  50%  { transform: translateY(-20px); }
  100% { transform: translateY(0); }
}

相同的帧可以合并:

css
@keyframes flash {
  0%, 50%, 100% { opacity: 1; }
  25%, 75%     { opacity: 0; }
}

clip-path

clip-path 用来裁剪元素的显示区域,配合 transition 能做出不规则的揭示动画。

circle() - 圆形

css
clip-path: circle(50%);              /* 以中心为圆心,半径 50% */
clip-path: circle(50% at 0% 50%);    /* 指定圆心位置 */

常见用法是 hover 时从某个点展开:

css
.card {
  clip-path: circle(0% at 50% 50%);
  transition: clip-path 0.5s ease;
}

.card:hover {
  clip-path: circle(100% at 50% 50%);
}

polygon() - 多边形

参数是各个顶点的坐标:

css
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);            /* 三角形 */
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);    /* 菱形 */

inset() - 矩形内缩

css
clip-path: inset(10px);                  /* 四边各裁 10px */
clip-path: inset(10px 20px 30px 40px);   /* 上右下左 */
clip-path: inset(10px round 10px);       /* 加圆角 */

性能优化

一句话原则:能用 transform 和 opacity 解决的,就别碰别的。

它们只触发合成,不会触发重排和重绘。其他属性多多少少都会拖累帧率。

避免动画这些属性width / heighttop / left / right / bottommargin / paddingfont-size。要改大小用 transform: scale() 代替。

will-change:提前告知浏览器

css
.will-animate {
  will-change: transform;
}

浏览器会提前做好优化准备,但不要滥用——用完要移除,否则一直占内存。

强制 GPU 加速

加一个 Z 轴上的变换,浏览器就会把元素扔到合成层:

css
transform: translateZ(0);
/* 或 */
transform: translate3d(0, 0, 0);

老技巧了,现代浏览器对 transform 默认已经处理得很好,只在确实需要时再用。

常见的坑

  • height: auto 不能 transition —— auto 不是数值,没法插值。要展开收起一般用 grid-template-rows: 0fr 1fr,或者测出像素值再过渡。
  • overflow: hidden 会裁掉子元素 transform 出去的部分 —— 经典 bug:hover 放大被切了一半,多半是父容器有 overflow: hidden
  • transform 顺序不可换 —— translate rotaterotate translate 结果不一样,前面已经讲过。

无障碍

部分用户开启了系统的「减少动效」(出于前庭功能、注意力等真实原因),可以用 prefers-reduced-motion 检测。非必要的动画都应该尊重这个设置:

css
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

或者只关掉装饰性动画,保留功能性反馈:

css
@media (prefers-reduced-motion: reduce) {
  .decorative {
    animation: none;
    transition: none;
  }
}

写在最后

动画应该让交互更清晰,而不是装饰。 如果一个交互去掉动画也没问题,那这个动画就要问问自己在做什么 —— 不是说要砍掉,而是要让它真的在传达信息(状态变了、东西出现了、操作被确认了)。

延伸阅读

工具 / 库

最佳实践

做得好的网站(找灵感)