
CSS动画其实是被很多人低估的一个领域。我见过不少前端老手做布局、写逻辑一点问题没有一碰到需要动效的需求就开始“面向搜索引擎编程”——transform怎么写animation的timing-function到底怎么调这些细节平时不显眼真要用的时候还真容易卡壳。市面上讲transform和animation的文章不少但往往要么只讲API不说原理要么篇幅太长读不完。这篇文章我想把它做成一份真正能当手册用的“属性大全”把两个属性的每个细节拆开讲透配合实战案例最后再附上我在实际项目中踩过的坑。文章面向的是对CSS有一定基础、但没系统整理过动画属性的前端开发。不管你是在做官网的滚动动效、移动端的交互动画还是想自己实现一些花哨的视觉特效这篇文章都适合通读一遍然后收藏备用。1. Transform属性核心拆解1.1 坐标系与变换的本质Transform翻译过来叫“变换”听起来高大上本质上就是CSS帮我们做了一次数学运算。每个元素在页面里都有位置坐标transform就是在元素渲染之前先对它的坐标系做一次处理然后把元素画到新的位置上。这里有个关键概念大家一定要理解清楚transform操作的是元素自身的坐标系而不仅仅是元素的位置。这听起来有点绕我打个比方。你手里拿着一张照片transform就是改变照片在你眼前的状态你可以左移右移、转个角度、放大缩小、甚至斜着放。但不管怎么变你看到的是这张照片被“投影”在页面上的样子照片本身的内容并没有变。坐标系是二维的所以任何transform操作都可以拆分成水平方向的X轴和垂直方向的Y轴来理解。translate(move)是移动rotate是旋转scale是缩放skew是斜切。这四个基本操作组合起来就构成了CSS transform的整个功能版图。1.2 四大变换函数逐一击破translate()负责平移语法是translate(tx, ty)tx是水平位移ty是垂直位移如果只写一个值另一个默认为0。这里最容易被忽略的是百分比单位——它相对的不是元素本身的宽高而是元素自身尺寸的百分比。所以一个宽度为200px的元素translateX(50%)移动的长度是100px而不是100%。这个特性在做水平垂直居中时非常实用.centered { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }rotate()负责旋转单位是deg度或turn圈。rotate(45deg)顺时针旋转45度rotate(45turn)则是45圈后者一般不常用。旋转是绕transform-origin变换原点进行的默认是元素的中心点也就是CSS中的“center center”。scale()负责缩放scale(1.5)表示整体放大1.5倍scale(1.5, 0.8)表示水平方向放大1.5倍、垂直方向缩小到0.8倍。注意缩放的是视觉尺寸不是布局尺寸——元素在文档流中占的位置是不会因为scale而改变的它只是被视觉上“拉伸”了。这个特性用来做hover放大效果很方便因为不会引起兄弟元素的布局抖动。skew()负责斜切skewX(30deg)让元素在水平方向发生30度的倾斜视觉效果是这个矩形被“推歪”了有点像平行四边形被从侧面推了一把。rotate()、scale()、skew()三个函数还各有3D版本rotateX/rotateY/rotateZ、scaleX/scaleY/scaleZ。这些需要搭配perspective属性才能看到3D景深效果2D场景下用不上后面讲性能时会再提。1.3 transform-origin变换原点详解transform-origin是transform的隐藏boss很多人写transform时完全忽略它结果旋转出来效果不对百思不得其解。默认的transform-origin值是50% 50%也就是元素的中心点。所有变换操作都以这个点为锚点进行。但有些场景需要改原点比如做一个时钟秒针的时候我们希望指针绕底部旋转而不是绕中心旋转这个时候就要把transform-origin改成bottom:.clock-hand { transform-origin: 50% 100%; /* 底部中心 */ animation: rotate 60s linear infinite; }再比如做卡片翻开效果时希望卡片沿左边翻转就需要设置transform-origin: left center。transform-origin接受两个值第一个是X轴位置第二个是Y轴位置也可以用top、right、bottom、left这些关键字。这个属性对rotate和scale影响巨大对translate则没有影响——因为平移根本不涉及“围绕哪个点”的问题这点要拎清楚。1.4 多重变换书写顺序的真相这可能是transform里最反直觉、也最容易被坑的地方。多个变换写在同一个transform属性里时执行顺序是从右往左的而且这个顺序对最终结果影响极大。我举一个直接能看出来的例子.box1 { transform: translate(100px, 0) rotate(45deg); } .box2 { transform: rotate(45deg) translate(100px, 0); }box1先旋转再平移box2先平移再旋转。视觉上的结果是box1移动到了右下方的位置而box2差不多移动到了正右边的位置。原因是顺序书写为translate(...) rotate(...)时元素先执行rotate建立新坐标系然后再translate时平移的方向已经被旋转过了。所以translate(100px, 0)实际移动的是旋转后坐标系的“右边”这个方向已经偏离了原始的x轴。理解了这一点你就知道为什么很多动画库的内部实现要把translate放在最后写为了保证“先移动再旋转”避免旋转影响平移的方向。提示当多个变换同时存在时建议把translate放在最前面rotate和scale放在后面。这样写的心理模型是“先把元素挪到目标位置再围绕自身做旋转和缩放”符合绝大多数设计意图。反过来写容易出各种“灵异现象”。1.5 性能问题为什么说transform是动画利器浏览器在渲染一个页面时CPU负责布局和绘制计算GPU负责把最终的画面合成并显示在屏幕上。通过修改width、height、top、left这些属性做动画时每帧都要重新触发布局和绘制性能开销大且容易掉帧。而transform的属性特点是它不改变布局浏览器可以把元素提升到一个独立的合成层直接交给GPU处理合成的开销要比重新布局小得多。为了让这个合成层真正生效实际上有一个隐藏条件元素上需要有独立的层。开发中常写的will-change: transform或者直接跑一个transform动画本身就是给浏览器一个“这个元素我要单独处理”的信号。实际项目里较大的顿挫感大多不是transform本身的问题而是滥用box-shadow和filter。这两种属性和transform一起用时GPU的内存占用会飙升低端手机上尤其明显。后面避坑章节我会展开讲。2. Animation动画属性全解析2.1 什么是keyframes以及两种写法animation动画是靠keyframes定义关键帧来驱动的。关键帧就是动画过程中某个时间点的元素状态浏览器会自动计算两个关键帧之间的过渡过程这个计算过程叫插值。keyframes有两种写法/* 方式一from/to */ keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } /* 方式二百分比 */ keyframes fadeStep { 0% { opacity: 0; } 50% { opacity: 0.5; } 100% { opacity: 1; } }from等效于0%to等效于100%。多阶段动画必须用百分比写法因为from/to只能描述起点和终点两个状态。如果动画需要在50%的位置切换一次颜色就必须用百分比。关键帧里可以写任意CSS属性但为了性能考量我只推荐在关键帧里使用transform和opacity两个属性。为什么回到上一节说的原理这两个属性的动画不会触发layout和paint直接在合成层完成。而animated width、height、top、left这些属性每帧都要走一遍完整的布局计算页面元素一多就“卡成PPT”。2.2 animation子属性逐个讲解animation一共有8个子属性。我用一张表先列出来然后挑几个容易出问题的展开讲子属性作用默认值animation-name指定keyframes名称noneanimation-duration动画时长0sanimation-timing-function速度曲线easeanimation-delay延迟时间0sanimation-iteration-count播放次数1animation-direction播放方向normalanimation-fill-mode结束后状态noneanimation-play-state播放/暂停runninganimation-duration和animation-delay都接受时间值但两者在简写里有一个顺序陷阱。在animation简写中第一个时间值会被识别为duration第二个时间值才是delay。写反了很难排查因为动画只是“卡住不动”不报任何错误。animation-iteration-count可以设置为infinite表示无限循环也支持小数比如0.5表示只播放前半段。animation-direction有四个值normal、reverse、alternate、alternate-reverse。normal正向播放reverse倒向播放alternate每轮交替方向第一轮正向第二轮反向如此循环alternate-reverse第一轮反向开始交替。做呼吸灯、上下浮动一类的循环动画时alternate非常好用比如浮动的入场和回位如果各写一遍关键帧就很啰嗦用alternate一行搞定。2.3 timing-function的速度魔法animation-timing-function控制的是动画过程中速度的变化曲线。除了常见的关键字ease、ease-in、ease-out、ease-in-out、linear之外还有两个很猛的写法cubic-bezier()和steps()。cubic-bezier()是三次贝塞尔曲线由四个参数控制cubic-bezier(x1, y1, x2, y2)。这个曲线决定了“时间是自变量、进度是因变量”的关系。看不懂数学也没关系Chrome浏览器的DevTools里点开样式面板的贝塞尔曲线图标可以用可视化拖拽的方式调整四个点实时预览效果调完直接复制代码。steps()则是做“逐帧动画”的灵魂函数。它会将动画的过渡过程切分为n个“阶梯”每个阶梯之间直接跳变不做平滑插值。语法是steps(n, jumpterm)n是阶梯数jumpterm控制跳变时机常用的是start和end。举个最直观的例子数字时钟的翻页效果。数字从0跳到1不是平滑过渡而是瞬间跳变。如果给这个跳变加上一个翻页的扭曲过程就成了之前网上很火的“苹果时钟翻页”效果。以后面试或者项目需要露一手的时候这个必杀技绝对好用.clock-flip { animation: flip 1s steps(1, end) infinite; } keyframes flip { 0% { transform: rotateX(0deg); } 100% { transform: rotateX(360deg); } }关于steps有个常见的误区是觉得“用了steps动画就一定会不流畅”。其实steps只是把连续变化的数值“切碎”成离散跳变每两个step之间没有中间状态。用在数字轮播、打字机、雪碧图帧动画上毫无压力用到进度条上就会一格一格地跳。2.4 fill-mode的前向与后向填充animation-fill-mode是最容易让新手“哇”一声的属性。它的作用在于控制动画结束前后元素应该显示什么状态。none动画结束元素回到没有动画时的初始状态forwards动画结束后元素保持动画最后一个关键帧的样式backwards动画还没开始还在delay中时元素先应用动画第一个关键帧的样式both同时具备forwards和backwards的效果举一个最常见的场景一个loading遮罩弹出来用fadeIn动画从opacity: 0渐变到opacity: 1。如果动画结束后不保持最终状态元素会瞬间回到opacity: 1的默认态——视觉上就是先淡入又突然“实”了一下如果动画设置为forwards元素就停在动画结束时的opacity: 1表现自然。delay配合backwards也很有用一个元素延迟2秒开始旋转动画如果不加backwards那么在动画尚未开始的2秒里元素显示的是没有动画时的默认状态可能在页面上闪一下加上backwards元素会提前进入动画第一帧的状态等待动画开始。2.5 animation简写语法与优先级细节与transform不同animation拥有简写属性可以一行写完所有子属性.element { animation: fadeIn 2s ease-in 0.5s 3 both alternate; }注意简写里的顺序第一和第二个时间值分别对应duration和delay其他关键字只要不冲突顺序自由。为了让代码可读性更好我个人习惯固定顺序name duration timing-function delay iteration-count direction fill-mode。有没有办法只改一个子属性而不覆盖整个animation可以单独写animation-play-state或者用animation的子属性单独声明。比如要暂停一个动画.element:hover { animation-play-state: paused; }这个在鼠标悬停时暂停动画、移开继续的需求里极常见。关于animation优先级要额外注意在简写形式里没写的子属性会重置为默认值。所以如果你想先写一行简写再单独声明animation-delay那没问题但如果你先写单独声明再写简写单独的声明就会被覆盖。顺序一颠倒坑就来了。3. 实战案例用Animation实现时钟翻页效果3.1 需求拆分与方案设计网上流传的时钟翻页动画很多人以为是纯CSS写的其实原版大多用了JavaScript定时器配合翻转结构。纯CSS只靠animation硬写一个走时的时钟并不现实因为CSS没有获取当前时间的能力但我们可以实现“翻转数字”这个动效本身。这个案例的巧妙之处在于数字跳变本身是瞬间的用steps翻转过程是平滑的用transform和animation配合。我们要实现的最终效果是一个数字卡片从0开始每秒翻动一次翻动过程中数字会从旧值切换到新值。需求拆分成三个部分数字的跳变由JS定时器控制数字内容变化翻动的动画效果由CSS animation控制时间的初始化由JS获取当前时间赋值对于纯CSS部分我实现一个“秒”位翻页卡片的动画逻辑HTML结构就用经典的上下两个半区div classflip-clock div classflip-card div classtop0/div div classbottom0/div /div /div3.2 翻页动效的CSS实现翻页的核心是让上半区和下半区在翻动时“错位闪动”。数字跳变的瞬间上半区向上翻起、下半区直接切换显示新数字。.flip-card { position: relative; width: 80px; height: 120px; perspective: 400px; border-radius: 8px; overflow: hidden; } .flip-card .top, .flip-card .bottom { position: absolute; left: 0; width: 100%; height: 50%; background: #222; color: #eee; font-size: 60px; display: flex; align-items: center; justify-content: center; } .flip-card .top { top: 0; align-items: flex-end; border-top-left-radius: 8px; border-top-right-radius: 8px; transform-origin: center bottom; } .flip-card .bottom { bottom: 0; align-items: flex-start; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; transform-origin: center top; } .flip-card.flipping .top { animation: flipTop 0.6s ease-in forwards; } keyframes flipTop { 0% { transform: rotateX(0deg); } 100% { transform: rotateX(-90deg); } }这里几个细节值得展开说。加了perspective: 400px之后rotateX才能表现出深度感——元素在3D空间中翻转时你能看到近大远小的透视变形而不是简单压扁。transform-origin设成center bottom是为了让上半区像真实的翻页动作一样以下边为轴向上翻起。翻完上半区后下半区直接显示新数字。从用户视角来看旧数字先“翻走”新数字立刻“出现”。这个动效用上animation-fill-mode的forwards确保翻起状态保持住不至于翻完又弹回去。如果想让动画更细腻下半区的“翻入”效果也可以做成一个完整的旋转.flip-card.flipping .bottom { animation: flipBottom 0.6s ease-out 0.3s both; } keyframes flipBottom { 0% { transform: rotateX(90deg); } 100% { transform: rotateX(0deg); } }这个效果的时序是0到0.3秒上半区翻起0.3到0.6秒下半区从趴着翻到直立。中间的delay通过animation-delay实现填写0.3s。这种“先上后下”的连贯错位就是高级翻页感的核心也让animation-delay在这里有了用武之地。3.3 JS辅助动态刷新与常见坑纯CSS解决不了“获取当前时间”的问题需要JS配合。核心逻辑是先获取当前时间的秒位数然后每秒更新一次数字function updateClock() { const now new Date(); const seconds String(now.getSeconds()).padStart(2, 0); const top document.querySelector(.flip-card .top); const bottom document.querySelector(.flip-card .bottom); const card document.querySelector(.flip-card); const prev seconds.slice(-1); if (top.textContent ! prev) { top.textContent prev; bottom.textContent prev; card.classList.remove(flipping); // 强制触发重绘确保动画能重新启动 void card.offsetWidth; card.classList.add(flipping); } } setInterval(updateClock, 1000); updateClock();这里面有一个典型的前端动画坑当你给同一个元素连续添加和移除某个class时如果浏览器把这两次操作合并到了同一帧动画就不会重新触发。解决办法是代码里的void card.offsetWidth强制浏览器进行一次reflow相当于告诉它“这里状态已经变化了重新开始”。这个技巧我在多个动画需求里都用过简单粗暴但有效。如果你把数字内容修改和class添加顺序写反还会出现新数字先显示、旧动画后执行的问题。正确逻辑是先改数字再把“翻动”类加上去让动画包含数字切换的瞬间。3.4 perspective注意事项做3D翻转效果时perspective要写在父容器上而不是动画元素本身。原因是perspective定义了“视点距离”是对整个3D空间的透视设置子元素才会在此基础上做3D变换。还有一个很容易忽略的点父容器如果设置了overflow: hidden子元素的3D效果会被裁切。因为overflow: hidden会强制元素成为二维平面perspective的深度信息在裁剪后全部丢失。我踩过一次这个坑做翻页日历的时候怎么调参数翻转效果都异常最后发现是外层容器加了一个border-radius和overflow: hidden的组合。解决办法是把overflow: hidden从3D上下文的外层挪走或者干脆不裁切、用圆角背景来模拟裁剪效果。4. 用JS动态操作Transform的实战技巧4.1 直接赋值style.transform前端经常会遇到“鼠标拖拽一个元素”“滚动时元素跟随偏移”这类需求。最直接的实现方式是修改元素的style.transformconst el document.getElementById(draggable); el.style.transform translate(${x}px, ${y}px);这里用模板字符串拼接坐标值省去了字符串拼接的麻烦。需要注意的一个细节是style.transform每次赋值都会覆盖之前的值。如果这个元素还有scale、rotate等需求就必须在每次赋值时把所有变换一起写进去el.style.transform translate(${x}px, ${y}px) scale(${scale});要是忘了把scale带上动画过程中元素尺寸会突然恢复默认视觉上“跳”一下。更好的做法是维护一个状态对象每次更新属性后统一重新生成transformconst state { x: 0, y: 0, scale: 1, rotate: 0 }; function applyTransform() { const { x, y, scale, rotate } state; el.style.transform translate(${x}px, ${y}px) scale(${scale}) rotate(${rotate}deg); }4.2 读取当前transform值读取transform的值比设置要麻烦一些。直接el.style.transform读到的可能都是空字符串因为内联样式没设置过。要用getComputedStyle来拿计算后的样式值const style window.getComputedStyle(el); const matrix style.transform;得到的matrix是一个4x4矩阵形如matrix(1, 0, 0, 1, 100, 0)。前四个数字是旋转和缩放的组合最后两个数字是translate的tx和ty。如果做了3D变换会变成matrix3d(...)的长串。手动从matrix里解析x和y也不难但没必要重复造轮子。很多手势库内部已经封装好了比如直接使用浏览器原生的DOMMatrix来解析const matrix new DOMMatrix(getComputedStyle(el).transform); console.log(matrix.m41, matrix.m42); // x, y 平移量m41对应矩阵第4行第1列在CSS变换的平移部分里就是x轴位移m42是y轴位移。这个技巧在做拖拽元素并需要“记住”上次位置时不至于把transform解析写崩。4.3 动态transform配合Tween的平滑过渡JS给style.transform赋值后如果想让数值变化“平滑”而不是“瞬跳”有两种方案。一是用CSS的transition属性加过渡#draggable { transition: transform 0.3s ease-out; }这样每次修改transform值时浏览器会自动从当前值过渡到新值。很多新手做轮播图拖拽回弹效果就靠这个。但transition有个问题如果元素被连续拖拽动画还没结束又给了新值就会导致卡顿和“抖动”。这时可以动态修改transition时长拖拽过程中设为0s紧跟随手松手后设回0.3s平滑滑入el.style.transition none; el.style.transform translate(${currentX}px, ${currentY}px); // 松手时恢复过渡 requestAnimationFrame(() { el.style.transition transform 0.3s ease-out; el.style.transform translate(${targetX}px, ${targetY}px); });另一种方案是使用requestAnimationFrame自己写匀速缓动函数比如常见的easeOutCubic。这种做法的优点是精细控制每一帧的位置适合实现复杂运动路径和曲线运动。纯CSS的transition很难精确控制在某个时刻做某个速度变化但JS写缓动函数就灵活得多。4.4 动态控制animation的启动与暂停除了通过JS修改transform动态控制animation也经常遇到。上一节翻页时钟里的class切换是一种方式更常见的需求是“页面滚动到某个位置时启动动画”。实现思路是监听scroll事件判断元素的getBoundingClientRect().top是否进入了视口命中则添加动画类。如果使用IntersectionObserver会更高效因为它不用每帧计算位置const io new IntersectionObserver((entries) { entries.forEach((entry) { if (entry.isIntersecting) { entry.target.classList.add(animate-in); } }); }, { threshold: 0.3 }); document.querySelectorAll(.scroll-animate).forEach((el) io.observe(el));如果需要在动画播放完后移除监听可以配合animationend事件el.addEventListener(animationend, () { el.classList.add(animated-done); });animationend是原生事件监听到动画结束立马处理后续逻辑这比setTimeout估时间靠谱得多。类似的事件还有animationstart和animationiteration每轮循环结束时触发。这三个事件在处理复杂动画编排时能省很多事。5. 常见问题与避坑实录5.1 transform后文字模糊transform: scale(1.5)放大后文字偶尔会出现边缘模糊或发虚的情况这在Windows高DPI屏幕上特别常见。原因是浏览器在缩放时对位图做了插值计算文字边缘被算法“补”出了半透明像素。我的解决方案分两步一是尽量使用整数倍缩放比如scale(2)而不是scale(1.5)这样能明显减少模糊二是在缩放结束后不再继续变化时考虑用一个备用机制比如用width/height替代并接受性能损失或者给元素加filter: blur(0)强制提高清晰度。后一种其实是小技巧有时候有效有时候无效最稳妥的办法还是有序地规划缩放倍数。如果动画过程难免产生模糊从用户角度其实感知不强短时间的模糊是可以接受的。5.2 transform建立新的坐标系与z-index加了transform的子元素在三维空间里的层级会重新计算。有一个经典bug父元素加了transform之后子元素的position: fixed不再相对于视口而是相对于父元素定位。原因就是transform创建了新的包含块containing block。举一个场景页面左侧导航栏设置为position: fixed希望它固定不动结果某天在父级容器上加了一个微不足道的transform比如为了做hover特效导航栏瞬间“脱离”视口改为相对父容器定位滚动时跟着跑。排查这个问题的过程很折磨人因为transform特效看起来和导航定位毫无关联。如果遇到类似问题排查顺序是检查这个fixed元素的所有祖先节点逐个加边框观察哪一层有transform。找到后有两个选择把transform从那个祖先上移开或者接受fixed被破坏的事实改成非定位方案。5.3 transform属性被覆盖与空格陷阱多个类名同时给一个元素设置transform时后覆盖的会整体覆盖前者而不是合并。比如.hover-effect { transform: scale(1.1); } .active-effect { transform: rotate(45deg); }这个元素同时有hover-effect和active-effect时只会显示rotate(45deg)scale不会生效。要解决只能合并类实现.active-effect { transform: scale(1.1) rotate(45deg); }还有一个容易忽略的细节transform字符串中的逗号后要不要空格很多老代码用translate(100px,0)或translate(100px, 0)两种写法浏览器都能解析。但如果在JS里拼字符串时把空格漏了或者多加了不同浏览器对容错有细微差异建议统一用标准写法每个参数后加一个空格。5.4 动画导致的GPU内存占用飙升transform的GPU加速有代价——每创建一个合成层都会消耗显存。一个页面几十个元素同时做transform动画GPU内存占用会涨得很快低端安卓机上通常表现为页面变卡、发热、甚至掉帧。减少层数量的策略是动画结束后主动释放。可以在animationend里移除will-change或者把transform置回noneel.addEventListener(animationend, () { el.style.willChange auto; });另一个策略是合并动画对一个容器整体做transform不要在它几十个子元素上各自做独立动画。子元素越多合成层越多性能越差。这也是我推荐“在父容器上做位移整体移动而不是每个子项单独位移”的原因。5.5 常见问题速查表现象原因解决方案transform旋转角度不对没有考虑transform-origin设置对应的origin多个变换叠加结果怪异变换顺序错误translate放最前rotate/scale放其后动画重开后不生效class添加移除在同一帧合并强制触发reflow如void offsetWidthfixed定位突然失效祖先节点有transform找到并移除祖先transform动画结束后元素归位没有设置fill-mode: forwardsanimation-fill-mode: both动画开始前元素闪现delay期间未应用初始状态animation-fill-mode: backwards/both简写中两个时间值错位第一个是duration第二个是delay检查书写顺序旋转时文字发虚非整数倍缩放/边缘插值用整数倍scale或接受短暂模糊5.6 动画性能优化的三条黄金法则第一条能用transform和opacity解决的动画绝不动width、height、top、left这些触发布局的属性。第二条减少同时运行的动画数量。页面上同时运行的合成动画最好不要超过5-6个超过之后低端设备的帧率会明显下降。真要同时跑很多动画考虑用canvas替代DOM动画。第三条把动画元素尽量集中管理。如果一个区块整体动效包含很多子元素的渐变、位移优先用一个父级元素统一transform避免大量独立合成层出现。这三条在我做移动端H5活动页时反复验证过遵守与否对流畅度的影响可以说是一个天一个地。6. transform与animation的进阶联动6.1 多个动画叠加运行的写法animation支持同时运行多个动画多个动画名通过逗号分隔.element { animation: fadeIn 1s ease-out, bounce 0.5s ease-in-out 1s; }这个特性在做“先淡入再弹跳”这种复合动画时非常有用不用自己写复杂的关键帧也不用JS事件监听。每个动画的时间线可以独立控制互不干扰。要注意的是多个动画作用于同一个属性时后面动画会覆盖前面动画的同属性效果。比如第一个动画设置opacity第二个动画也设置opacity最终的opacity以第二个为准。6.2 从animation到CSS Motion Path的扩展如果觉得transform能做的东西还不过瘾可以了解下CSS Motion Path运动路径。它能让元素沿着一条自定义的SVG路径运动语法核心是offset-path属性。这个API目前主流浏览器已经支持做飞机航线、汽车导航、跑酷小人等复杂轨迹动画时比手动计算transform逐帧控制位置要优雅得多.moving-element { offset-path: path(M0,0 C100,50 200,150 300,100); animation: move 3s ease-in-out infinite; } keyframes move { 0% { offset-distance: 0%; } 100% { offset-distance: 100%; } }这个方向很适合做网站的“数据可视化大屏”动效。我的习惯是路径动画用Motion Path位移/旋转/缩放用transform行为触发和状态切换用animation配合class切换。三者各司其职代码组织起来很清晰。6.3 transform在UI组件里的典型用法前端做UI组件的时候transform几乎无处不在。模态框居中用translate(-50%, -50%)下拉菜单展开用scaleY旋转变换tab切换滑动条用translateX滑来滑去loading图标用rotate做旋转按钮按下效果用scale(0.95)压一下。我自己维护过一个组件库自己的经验是凡是需要状态切换的组件打开/关闭、展开/收起、切换/滑动优先考虑用CSS动画配合transform实现而不是JS去改一堆样式。这样不仅代码量少性能更好而且调试时直接在样式面板里改参数就能预览效果。这里给出一个模态框弹出动画的完整示例.modal { transform: translate(-50%, -50%) scale(0.8); opacity: 0; transition: transform 0.3s ease-out, opacity 0.3s ease-out; } .modal.show { transform: translate(-50%, -50%) scale(1); opacity: 1; }注意translate(-50%, -50%)必须在两种情况里同时出现否则动画过程中元素中心点会偏移。这个看起来简单的问题实际项目里写错的人非常多因为中心定位时忘了保留translate部分一加transition就会发现元素在动画过程中“漂移”了。7. 写在最后从transform的坐标系到animation的八个子属性再到两者的联动听起来内容不少但只要顺着“坐标—变换—插值—关键帧”这个思路去看其实是一个连贯的整体。transform解决的是“元素在什么位置以什么形态呈现”animation解决的是“从一种形态平滑过渡成另一种形态”两者配合就构成了现代网页交互动效的基础底座。我个人做了这么多年前端最深的体会是CSS动画的性能上限其实非常高很多“看起来只能靠JS库实现”的效果纯CSS都能做到只是需要我们对transform和animation有很底层的理解。尤其遇到H5页面在低端机上的性能瓶颈纯CSS方案往往是比引入动画库更优的解法。如果你读完这篇之后能自己动手实现几个小动画——翻页时钟、拖拽回弹、列表入场——再把文章里提到的坑逐一踩一遍那这份“属性大全”对你来说就真正落地了。CSS动画的乐趣恰恰在于掌握规则之后翻翻常见效果你自然就知道它大概是怎样从规则里长出来的。