前阵子写几个 hover 动画时,发现常用的 CSS 属性都有点生疏了。趁这次把 opacity、transform、transition、animation、@keyframes、clip-path 这几个最常打交道的捋一遍,当备忘。
🌟 一句话前置:动画首选
opacity和transform,它们只触发合成(Composite),不会触发重排和重绘,性能最好;其他属性能不动就别动。
opacity
透明度,0 ~ 1。看起来只是个数值,但配合 transition 就能做出很自然的淡入淡出效果:
.fade-card {
opacity: 0.4;
transition: opacity 0.4s ease;
}
.fade-card:hover {
opacity: 1;
}
也可以配合 @keyframes 做循环呼吸效果,常见于通知红点、加载提示:
.dot {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
⚠️ 小坑:
opacity: 0的元素仍然可点击、仍会被屏幕阅读器读到。要彻底隐藏要配合visibility: hidden、pointer-events: none或aria-hidden。
transform
让元素移动、旋转、缩放、倾斜,全都走 GPU。
translate - 位移
transform: translateX(100px);
transform: translateY(50px);
transform: translate(100px, 50px);
transform: translate3d(100px, 50px, 0); /* 强制 GPU 加速 */
要移动元素就用 translate,不要改 left/top —— 后者会触发重排。
rotate - 旋转
transform: rotate(45deg); /* 顺时针 */
transform: rotate(-45deg); /* 逆时针 */
transform: rotateX(45deg); /* 沿 X 轴翻转,有 3D 效果 */
transform: rotateY(45deg);
scale - 缩放
transform: scale(1.5);
transform: scale(0.5);
transform: scaleX(2);
按钮的点按反馈也是 scale 的常见用法 —— hover 时变色提示可点,按下时缩一点给出物理反馈:
.card:hover { background: #2a2a2a; }
.card:active { transform: scale(0.96); }
组合 & 顺序
多个变换写在一起,顺序会影响结果:
transform: translateX(100px) rotate(45deg); /* 先平移,再就地旋转 */
transform: rotate(45deg) translateX(100px); /* 先旋转坐标系,再平移,会画出弧 */
transform-origin
默认以元素中心为原点。要换原点用 transform-origin:
transform-origin: top left; /* 左上角 */
transform-origin: center; /* 中心,默认值 */
transform-origin: 50% 100%; /* 底部中心 */
transition
transition 是最简单的动画方式 —— 属性值一变,它就自动补出过渡。完整语法:transition: property duration timing-function delay;。
.button {
background: #333;
transition: background 0.3s ease;
}
.button:hover {
background: #666;
}
四个子属性
transition-property:要过渡的属性,all表示全部transition-duration:时长,如0.3s、300mstransition-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 直接抄就行。
/* 弹性效果 */
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-state:running/paused,用来暂停
简写小技巧
时长建议单独写 animation-duration,其他用简写。原因是简写里 duration 和 delay 顺序最容易写反,单独拎出来不会错。
.loading {
animation: spin linear infinite;
animation-duration: 1s;
}
@keyframes
两种写法。简单两帧用 from / to:
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
多阶段用百分比:
@keyframes bounce {
0% { transform: translateY(0); }
50% { transform: translateY(-20px); }
100% { transform: translateY(0); }
}
相同的帧可以合并:
@keyframes flash {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: 0; }
}
clip-path
clip-path 用来裁剪元素的显示区域,配合 transition 能做出不规则的揭示动画。
circle() - 圆形
clip-path: circle(50%); /* 以中心为圆心,半径 50% */
clip-path: circle(50% at 0% 50%); /* 指定圆心位置 */
常见用法是 hover 时从某个点展开:
.card {
clip-path: circle(0% at 50% 50%);
transition: clip-path 0.5s ease;
}
.card:hover {
clip-path: circle(100% at 50% 50%);
}
polygon() - 多边形
参数是各个顶点的坐标:
clip-path: polygon(50% 0%, 0% 100%, 100% 100%); /* 三角形 */
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); /* 菱形 */
inset() - 矩形内缩
clip-path: inset(10px); /* 四边各裁 10px */
clip-path: inset(10px 20px 30px 40px); /* 上右下左 */
clip-path: inset(10px round 10px); /* 加圆角 */
性能优化
一句话原则:能用 transform 和 opacity 解决的,就别碰别的。
它们只触发合成,不会触发重排和重绘。其他属性多多少少都会拖累帧率。
避免动画这些属性:width / height、top / left / right / bottom、margin / padding、font-size。要改大小用 transform: scale() 代替。
will-change:提前告知浏览器
.will-animate {
will-change: transform;
}
浏览器会提前做好优化准备,但不要滥用——用完要移除,否则一直占内存。
强制 GPU 加速
加一个 Z 轴上的变换,浏览器就会把元素扔到合成层:
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 rotate和rotate translate结果不一样,前面已经讲过。
无障碍
部分用户开启了系统的「减少动效」(出于前庭功能、注意力等真实原因),可以用 prefers-reduced-motion 检测。非必要的动画都应该尊重这个设置:
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
或者只关掉装饰性动画,保留功能性反馈:
@media (prefers-reduced-motion: reduce) {
.decorative {
animation: none;
transition: none;
}
}
写在最后
动画应该让交互更清晰,而不是装饰。 如果一个交互去掉动画也没问题,那这个动画就要问问自己在做什么 —— 不是说要砍掉,而是要让它真的在传达信息(状态变了、东西出现了、操作被确认了)。
延伸阅读
工具 / 库
- easings.net — 缓动函数预览,直接复制
cubic-bezier() - tw-animate-css — Tailwind 风格的动画库,内置大量效果
- tailwindcss-radix — 把 Radix 组件状态转成 Tailwind 类名
最佳实践
- Animata - Best Practices — 动画性能、无障碍、时长缓动的实用 checklist
做得好的网站(找灵感)