做前端这行,几乎每天都要和 CSS3 打交道。很多人一听到 CSS3 就想到圆角、阴影、渐变,但真正把页面盘活、把用户体验做细腻的,其实是 2D转换和动画这两块。CSS3 的 transform 属性可以平移、旋转、缩放、斜切元素;配合 transition 或 @keyframes 动画,又能让这些变化在时间轴上自然发生。这篇文章我把定义、实操、性能、避坑一次性讲透,适合刚起步的前端/页面仔,甚至做 PPT 动画、动效设计的同学也可参考。别怕基础,我会从最常用写法讲起,再逐步落到完整案例和排查技巧。
1. 先把概念钉死:CSS3是什么,2D转换和动画各解决什么问题
1.1 CSS3不是一门新语言,而是一组模块化能力
CSS3 的全称是 Cascading Style Sheets Level 3,它没有像 ES6 那样一份独立的“标准文档”,而是被拆分成了几十个独立模块,比如 Selectors、Backgrounds and Borders、Transforms、Transitions、Animations 等。每个模块各自更新迭代,所以你会发现有些 CSS3 属性很早就支持,有些到近几年才稳定。
这种模块化设计的好处是:你可以只挑需要的部分用,不用等整个语言升级。对于业务开发来说,CSS3 就是一种用样式描述视觉状态的语言扩展,圆角、阴影、渐变、弹性布局、网格布局、动画都可以算在广义 CSS3 里。而本文聚焦的三个关键词更具体:定义是理解 CSS3 的定位,2D转换解决元素“在平面空间里如何变形”的问题,动画解决“变形如何随时间发生”的问题。
1.2 2D转换:改变元素形态而不动文档流
transform 是 CSS3 里的核心属性,它能够对一个元素进行平移、旋转、缩放、斜切等二维空间操作。和直接改 margin、left、width 这些属性最大的区别在于:transform 不改变元素在文档流里占的位置,也不会触发重新布局。它更像是给元素贴了一张“视觉投影”,原件还在原处,但我们看到的是经过变换后的样子。
这个特性在动画里极其重要。因为浏览器在渲染时,操作 transform 通常可以绕开 layout 阶段,直接在合成阶段完成,性能远高于改 left/top。你可以把文档流理解成家具的摆放位置,把 transform 理解为给家具拍了一张透视照片,移动照片比真的搬家具快得多。下面两节里,我会把 2D转换每个函数的行为、坑点、组合顺序都过一遍。
1.3 动画的双层结构:过渡与关键帧动画
CSS3 的动画能力被人为分成两条路径。transition 是过渡,它描述的是“当属性值从一个状态变成另一个状态时,中间过程怎么展现”。比如按钮 hover 时颜色从浅变深,你只需要在样式里声明 transition: background-color .3s ease,浏览器就会自动补帧。它适合简单的状态切换,优点是代码少、好维护。
另一条路径是 @keyframes + animation:你可以自己定义多个时间点的关键帧,然后控制动画的运行次数、方向、延迟、结束状态。它适合复杂、循环、需要精确控制的多阶段动画。transition 和 animation 不是互斥的,实际项目中经常配合使用:用 transition 处理交互反馈,用 animation 处理进场、loading、循环漂浮这类场景。
2. 2D转换实操:transform的五个函数和组合顺序
2.1 五个函数逐个拆解:translate、rotate、scale、skew、matrix
transform 支持多个函数,以下是 2D 场景最常用的五个。
translate 负责位移。translate(tx, ty) 里的 tx 是横向位移,ty 是纵向位移,单位通常用 px,也可以写百分比。注意百分比是相对于元素自身尺寸的,比如 translateX(50%) 让元素向右移动自身宽度的一半,这个特性经常被用来做“绝对定位居中”:
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);rotate 负责旋转,单位是 deg,可以是正负值。正数按顺时针转,负数是逆时针。比如 rotate(45deg) 旋转 45 度。要注意旋转会连同坐标系一起转,后面接其他函数时,顺序会影响结果。
scale 是缩放,scale(sx, sy),只传一个值时 sy 默认等于 sx。它改变的是元素在平面内的尺寸比例。scale 不会改变文档流占位,这既是优点也是坑点,下面单独讲。
skew 是斜切,skew(xAngle, yAngle),让元素在 x 轴、y 轴方向发生倾斜,做平行四边形的标签、动态海报效果很常用。matrix 则是以上变换的矩阵统一形式,共六个参数:matrix(a, b, c, d, tx, ty),对应 2D 变换矩阵。大部分时候不必直接写 matrix,但理解它能帮你搞清组合顺序的本质。
| 函数 | 作用 | 常用单位/参数 |
|---|---|---|
| translate(tx, ty) | 位移 | px、%、单个值 |
| rotate(angle) | 旋转 | deg、turn |
| scale(sx, sy) | 缩放 | 数字、无单位 |
| skew(xAngle, yAngle) | 斜切 | deg |
| matrix(a,b,c,d,tx,ty) | 矩阵综合变换 | 数字 |
2.2 transform-origin:旋转缩放的“支点”决定了效果
transform 默认的变换中心是元素的中心点,也就是 transform-origin 的默认值 50% 50%。但很多效果需要改变中心点,比如钟摆旋转应该以顶部为支点,图片预览的缩放应该以鼠标位置为原点。
transform-origin 可以接收关键词 left、center、right、top、bottom,也可以接收具体长度或百分比。它直接影响 rotate 和 scale 的结果:同样的 rotate(45deg),中心点旋转看起来是原地转,左上角旋转则像门扇打开。元素 hover 放大时,如果用默认中心点,会从中间向四周扩展;如果设置 transform-origin: left center,则元素会从右往左延伸,视觉上更像“展开”。
实际开发里,卡片翻转、loading 圆环、进度条填充都离不开 transform-origin。建议在动手前先想清楚:“我期望哪个点固定不动?”这个点就是你的 origin。记不住规则时,可以打开浏览器开发者工具,在 Styles 面板里修改 transform-origin 实时观察,比硬啃文档效率高。
2.3 scale缩放方案的落地细节
热词里有个“css3:scale 缩放方案”,说明大家经常在缩放上栽跟头。最常见的需求是 hover 图标放大、图片列表悬停展示细节、整体页面适配缩放。scale 的优点是流畅、不回流,但有几个细节要留意。
第一,scale 后的元素宽度高度在布局尺寸上没变,周围元素不会避让。如果希望放大后盖住别人,记得给元素设置合适的 z-index,避免被裁切或遮挡。第二,缩放后文本和边框会一起变,还原成视觉上的“拉伸”,如果只需要扩大背景,应该用宽高过渡而不是 scale。第三,半透明边缘和 1px 线条在缩放时容易出现模糊或锯齿,可以用整倍数缩放(比如 scale(2) 配合 transform-origin: top left 做高清缩放预览)来减少模糊。
还有一点经常被忽略:图片 hover 用 scale(1.05) 时,容器最好设置 overflow: hidden,并给图片预留足够的过渡空间,不然图片会把容器撑破或者出现难看的滚动条。缩放中心也用 transform-origin 控制,比如希望图片从左下角放大,就写 transform-origin: left bottom。
2.4 组合变换的顺序:先scale还是先rotate,结果完全不同
很多人以为 transform: scale(2) rotate(45deg) 和 transform: rotate(45deg) scale(2) 等价,事实不是这样。CSS 变换函数的执行顺序是从右到左,可以理解为“从右往左给元素套坐标系变换”:后面的函数先作用于元素的局部坐标系,前面的函数再作用于整体结果。
举个例子,transform: translateX(50px) scale(2),语义上是先缩放两倍,再向右移动 50px,结果是移动距离还是 50px,但元素视觉大小是 2 倍;transform: scale(2) translateX(50px) 则是先移动 50px 再整体缩放,因为缩放发生在前面,位移距离同样被放大了,视觉上会移动 100px。
我在项目里常用的约定是:先写位移,再写旋转/缩放,最后写 skew。也就是 translate → rotate/scale → skew。这样可以保证位移不受后续缩放干扰,而且便于别人阅读。如果必须反过来,就拿 matrix 计算或者用 CSS 变量统一管理,而不是在样式里碰运气。
3. 动画实操:transition到animation的进阶之路
3.1 transition:让状态改变丝滑起来
transition 适合把 hover、focus、class 切换这类状态变化变得顺滑。它由四部分组成:transition-property、transition-duration、transition-timing-function、transition-delay。简写时中间用空格分开,多条属性用逗号分隔。
实际项目中我习惯这样写:
.btn { background-color: #4a90e2; transform: scale(1); transition: background-color .25s ease, transform .25s ease; } .btn:hover { background-color: #357abd; transform: scale(1.05); }这里同时过渡背景色和缩放,响应鼠标悬停时按钮有“浮起”的反馈。要注意两点:transition 必须写在起始状态上,而不是 hover 状态;transition 不会自动触发,只有当属性值发生变化时才会生效。
如果你希望多个属性的过渡节奏不一样,可以分别定义。比如 transform 用 0.3s 的 ease-out,background 用 0.2s 的 linear。过度使用 transition 会让交互显得拖沓,一般菜单、按钮控制在 0.2s~0.3s,卡片展开可以到 0.4s~0.5s。
3.2 @keyframes + animation:多阶段、循环、自定义时序
当动画不止两个状态,或者需要循环播放时,就要用动画属性。先定义关键帧:
@keyframes float { 0% { transform: translateY(0); } 50% { transform: translateY(-20px); } 100% { transform: translateY(0); } }然后把它挂到元素上:
.cloud { animation: float 6s ease-in-out infinite; }animation 的完整属性包括 name、duration、timing-function、delay、iteration-count、direction、fill-mode、play-state。其中几个容易踩坑:
- iteration-count 写成 infinite 表示无限循环;
- animation-direction: alternate 可以让动画在来回间交替播放,比如 hover 卡片上下浮动;
- fill-mode: forwards 可以让动画结束后停留在最后一帧,否则会瞬间跳回起始状态;
- animation-play-state: paused 可以用来配合 JS 控制播放/暂停,页面离开可视区域时暂停动画很有用。
关键帧不一定要写 0% 和 100%,animation 默认会用元素当前样式填充缺失的一头。但如果希望动画结束后保持最后一帧,还是显式加上 forwards 更稳妥。建议把动画时长、缓动函数抽成 CSS 变量,后续调整统一改变量即可。
3.3 数字滚动、元素进入视口时触发的动画
热词里“css3 元素可见时 数字展示”和“制作数值增长”常常是一起出现的需求:页面滚动到某个模块后,数字从 0 滚动到目标值。这个需求纯 CSS 其实做不了计数更新,必须配合一点 JS,但视觉衔接可以用 CSS 完成。
我的做法是这样的:
.count { transition: opacity .6s ease; opacity: 0; } .count.active { opacity: 1; }然后用 IntersectionObserver 监听目标元素:
const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('active'); startCountUp(entry.target); observer.unobserve(entry.target); } }); }, { threshold: 0.3 }); document.querySelectorAll('.count').forEach(el => observer.observe(el));startCountUp 里用 requestAnimationFrame 做数值递增,每帧把 content 更新成中间数字。如果想让数字变化更有“动效感”,可以配合 CSS filter 或者轻微 scale。这里的关键是:CSS 负责出现/消失的过渡,JS 负责数字本身,两者各司其职,代码也好维护。
另外,如果数值是整数,可以只做整数递增。如果是金额、百分比,需要在小数位数和千分位上做格式化,避免数字跳动时位数忽多忽少。可以在数字容器上设置等宽字体,或者用 tabular-nums 字体特性来减少宽度抖动。
3.4 loading动画与CSS动画库的选型
loading 动画是 CSS3 最典型的应用场景。最简单的旋转 loading 只需要一个圆环加 animation:
.spinner { width: 32px; height: 32px; border: 4px solid rgba(0,0,0,.1); border-top-color: #4a90e2; border-radius: 50%; animation: spin .8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } }如果要做骨架屏,可以用一个浅色块加 background 渐变动画循环移动。纯 CSS 做 loading 的好处是无依赖、体积小、可控性强,适合按钮、局部刷新的场景。
成熟项目里也会用 animate.css 这样的动画库。animate.css 提供了很多现成的进出场动画类,比如 bounceIn、fadeInUp,接入快,适合快速原型和运营活动页。但它的体积相对大,而且为了通用性,很多动画你可能用不到。更推荐的做法是把常用的几个动画从源码里抽出来,放进自己的动画工具类里。
像“播放json动画”属于另一条技术线,通常用 Lottie/SVGA 播放导出后的 JSON 动效。它和 CSS 动画的定位不同:CSS 适合界面交互、按钮、Loading;JSON 动画适合插画角色、复杂动态品牌展示。选择方案时先问自己:这个动效是否可以被拆成几个独立的 transform/opacity 阶段?如果可以,CSS 就够了;如果不行,再做 JSON。
4. 性能与兼容性:不卡顿、不踩坑的细节
4.1 为什么推荐只用 transform 和 opacity 做动画
浏览器渲染一帧会经历样式计算、布局、绘制、合成。如果动画改变的是 width、left、top 这类会产生布局的属性,浏览器每帧都要重新计算布局和绘制,代价非常高。而 transform 和 opacity 这两个属性在绝大多数现代浏览器里可以进入合成器线程,直接操作已经绘制好的图层,省掉了布局和绘制。
可以这样理解:改 left 等于把一整面墙重新粉刷;改 transform 等于用投影仪把一张海报平滑地移到新位置。所以做动画时,优先考虑用 transform 代替 top/left,用 opacity 代替 display:none 的显隐切换。
有经验的做法是:把所有可动元素都拆成独立图层,用 transform 做位移、旋转、缩放;用 opacity 做淡入淡出;背景色渐变这种不得已的情况,再考虑过渡背景色。这条原则能解决大部分页面卡顿问题。
4.2 动画卡顿排查:will-change、合成层、强制GPU
如果你发现动画在低端机上掉帧,第一步不要急着加 will-change,而是先看是不是属性用错了。打开 DevTools 的 Performance 录制一段动画,看每帧是否出现紫色或绿色的“Layout/Paint”开销。如果都是白色或灰色,说明渲染压力不大,掉帧可能来自主线程的 JS。
真正需要提升合成层时,可以用 will-change: transform 提前告诉浏览器这个元素将要发生变换,让它提前准备图层。但 will-change 不能滥用,每个元素都加会占用大量内存,反而更卡。还有一种老办法是给元素加 transform: translateZ(0) 强制创建合成层,现在基本不需要了,只有在特殊兼容场景才值得尝试。
实际排查时,我一般先减少动画影响范围。比如列表项有 50 个同时做位移动画,可以让容器整体移动,而不是每一项单独动。一次性动画和循环动画也要区别对待,循环动画尽量保持属性稳定,避免每次都创建新图层。
4.3 兼容性与降级方案:@supports、prefers-reduced-motion
CSS3 的 transform、transition、animation 基础属性在现代浏览器里已经非常稳定,但遇到老旧内核或者需要兼容更早的 Android WebView 时,还是建议加厂商前缀。现在主流构建工具会自动加前缀,但如果手写页面,可以保留 -webkit-transform 这类写法。
更重要的是做降级。不是每个用户都需要动效,也不是每个设备都跑得动。用 @supports 判断浏览器是否支持 transform,不支持时直接展示静态样式:
@supports (transform: rotate(0deg)) { .box { transform: rotate(45deg); } }另外,很多操作系统和浏览器提供了“减少动态效果”的偏好设置。CSS 里可以用 prefers-reduced-motion: reduce 主动关闭动画,这对晕动症用户非常重要:
@media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }这段规则会几乎瞬间完成所有过渡,用户看不到大幅移动,但页面仍然可用。在面向 C 端用户的页面里,这是我认为最有价值的一条兼容性最佳实践。
5. 完整案例:一个中秋贺卡动画页面的制作思路
5.1 结构设计:DOM、CSS变量、动画拆分
热词里有一条“中秋网页代码(html+css,打开就是中秋动画页面)”,这种节日贺卡页面很适合练手。我的设计思路是:一张深蓝夜空背景,左下角一轮圆月,几片云向右缓慢漂移,中部一句祝福文字淡入。整体动画拆成四个独立部分:月亮缓慢升起、云朵连续横移、星光闪烁、祝福文字上浮淡入。
CSS 变量统一管理动画时长和缓动:
:root { --moon-duration: 12s; --cloud-duration: 18s; --text-duration: 2s; --ease-slow: ease-in-out; }所有元素都设置为 absolute 定位,容器高度用 100vh 撑满,避免不同屏幕下布局抖动。拆动画时遵循前面说的原则:位移和缩放都用 transform,显隐用 opacity,不用改 top/left。
5.2 关键代码:月亮位移、云朵漂浮、祝福文字淡入
下面是一个可运行的简化版 HTML + CSS。你复制到本地,保存为 mid-autumn.html,双击就能在浏览器里看到效果:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>中秋贺卡</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { height: 100vh; overflow: hidden; background: linear-gradient(180deg, #0b1c3a 0%, #1a2f5a 100%); position: relative; display: flex; align-items: center; justify-content: center; } .moon { position: absolute; left: 50%; top: 50%; width: 120px; height: 120px; margin: -200px 0 0 -60px; background: #f7ecc9; border-radius: 50%; box-shadow: 0 0 60px 20px rgba(247, 236, 201, 0.35); transform: translateY(-60px); opacity: 0; animation: moon-rise 6s ease-out forwards; } @keyframes moon-rise { 0% { transform: translateY(60px); opacity: 0; } 100% { transform: translateY(-60px); opacity: 1; } } .cloud { position: absolute; top: 30%; left: -200px; width: 200px; height: 60px; background: rgba(255, 255, 255, 0.12); border-radius: 999px; filter: blur(2px); animation: cloud-move 18s linear infinite; } .cloud:nth-child(2) { top: 55%; width: 280px; animation-duration: 26s; animation-delay: -8s; opacity: 0.6; } @keyframes cloud-move { from { transform: translateX(0); } to { transform: translateX(calc(100vw + 400px)); } } .greeting { color: #fff; font-size: 36px; letter-spacing: 8px; text-shadow: 0 2px 12px rgba(0, 0, 0, 0.4); opacity: 0; animation: fade-up 2.5s 1s ease-out forwards; } @keyframes fade-up { 0% { transform: translateY(24px); opacity: 0; } 100% { transform: translateY(0); opacity: 1; } } </style> </head> <body> <div class="moon"></div> <div class="cloud"></div> <div class="cloud"></div> <p class="greeting">中秋快乐</p> </body> </html>这里的月亮动画在 6 秒内从下方移动到上方并淡入,动画结束后因为 fill-mode: forwards 而停在终点,不会闪回。云朵用时长为 18s 和 26s 的无限循环,通过 negative animation-delay 让第二片云一开始就出现在中间,形成错落感。祝福文字延迟 1s 才开始,让月亮先出现,再展示文字。
5.3 实际调整过程:时长曲线、元素层级、首屏体验
做完初版后,我通常会做三件事。第一,检查缓动函数。月亮升起用 ease-out 会比 linear 更有“慢慢停稳”的感觉;云朵横移用 linear 才是匀速,避免忽快忽慢。动画曲线的选择直接影响情绪,节日页面希望柔和,多用 ease-in-out。
第二,检查层级和裁切。云朵如果 z-index 低于月亮,会从月亮后面穿过,看你想要哪种效果;如果想让云遮挡月亮,就把云朵的 z-index 调高并降低透明度。第三,检查首屏体验。打开页面瞬间,所有动画同时启动会有点乱,给不同元素设置不同的 delay,让动画有先后顺序。关键是不要出现长时间空白,月亮升起的前两秒可以搭配背景渐变闪烁效果,让页面一打开就有反馈。
6. 常见问题与排查技巧实录
6.1 transform 不生效的几个典型原因
最常碰到的情况是元素本身不支持 transform,比如 span 默认是内联盒子,transform 可能不生效,改成 display: inline-block 或 display: block 就会好。其次是拼写问题,属性名写成了 transform: translate(10px, 10px) 却缺少单位,或者 rotate(45) 少了 deg,浏览器会忽略整个声明。
还有一种隐蔽的情况是祖先设置了 overflow: hidden,元素变换后被裁掉了。特别是位移或放大时,你以为 transform 没生效,其实是内容被容器裁掉了。另外,如果动画播放中 transform 被更高优先级的样式覆盖,也会出现突然跳回初始位置的现象。排查时先在 DevTools 里确认计算样式里 transform 到底有没有值,再逐层检查父容器。
6.2 transition 没变化 / 动画结束后跳回的问题
transition 没变化的大部分原因是:你写在了目标状态上,或者属性值改变时浏览器没识别出来。比如从 height: auto 过渡到固定高度,auto 无法参与过渡,需要先给一个确定值。还有 display: none 到 block 之间不会过渡,因为元素从无到有没有中间状态。
动画结束跳回常见的原因是缺少 fill-mode: forwards。另一个原因是关键帧只写了 0% 或 100%,浏览器用默认样式补齐另一头,但如果你希望结束状态是某一帧,必须显式声明。用 animation 做无限循环时,如果想在最后一帧和第一帧之间平滑衔接,要保证首尾关键帧的样式一致,或者用 alternate 来回播放。
6.3 表格、内联元素、fixed定位的动画坑
fixed 定位元素如果祖先有 transform、filter、perspective 等属性,它的定位参照就不再是视口,而是那个祖先容器。这是很多人做弹窗时发现的坑:弹窗 fixed 到页面,但因为父容器加了 transform: scale,弹窗位置和大小都被影响了。解决方法是把弹窗结构移到 body 下,或者让父容器不要包住弹窗。
表格 cell 元素直接做 transform 也可能表现不一致,最好把动画放在更外层的容器上。内联元素的位移、旋转虽然很多浏览器能显示,但可用性不稳定,统一用 inline-block。做拖拽移动、模态框缩放时,建议给目标元素一个专门的 class,集中控制 transform,而不是靠内联样式散落各处。
6.4 动画库体积与工作流的取舍
如果你在团队里负责动画,建议早点建立一条轻量工作流:设计给出动效描述,前端用 CSS 变量统一登记时长/缓动;遇到复杂插画动效才考虑 JSON 动画方案。animate.css 这类库适合原型阶段快速验证,真正上线前抽出用到的动画,或者干脆手写,能省下几十 KB。
JSON 动画(比如 Lottie)虽好,但它的播放依赖运行时,文件往往比 CSS 动画大不少,适合 Logo、吉祥物等品牌级复杂动效。简单的按钮、Loading、卡片飘入,老老实实用 transform + opacity 做完,性能和可维护性都会更好。动画工作流里最怕的是“为了动而动”,一个页面塞满各种进场的库依赖,加载慢、体验差,最后还得回头删。
做 CSS3 动画这几年,我最大的体会是:别追求复杂的动画方案,把 transform、opacity、过渡曲线这三个基本功练扎实,市面上七八成的界面动效都能解决。碰到数字滚动、元素进场这样偏交互的动画,也不必硬用纯 CSS 硬扛,合理搭配一点 JS,边界会清楚很多。每次做动画前,先问自己一句“它为什么动”,回答得上来再动手,最后效果差不到哪去。