做了六年前端,收藏夹里躺着几百个loading动画的链接,平时团队做项目我翻得最多的也是这类素材。前阵子整理资料,我把手头攒下的150款CSS3 loading加载动画重新按风格、动画原理、适用场景过了一遍,发现很多收藏其实处于“躺灰”状态,真到要用的时候还是只会那几个转圈圈。这篇文章我准备把这批动画的底细拆开讲清楚:它们常见的长什么样、核心动效是怎么写的、选型时该看哪些维度、怎么封装成一套真正能落地切换的组件,以及我在实际接入中踩过的坑。适合正在搭前端组件库的同学、被交互细节折磨的页面仔,也适合刚学CSS3动画想找练手素材的初学者。
1. 先说清楚:为什么是150款,而不是15款
1.1 一个loading背后是几类完全不同的设计语言
很多人觉得loading动画就是“转圈圈”,其实不是。你把150款放在一起扫一遍,会发现它们之间的差异比想象中大得多。有的走纯几何路线,用几个色块做旋转和缩放;有的走拟物路线,模仿沙漏、进度条、刷新箭头;有的走文字路线,把字母拆开做逐个位移;还有一类是骨架屏思路,用背景渐变模拟内容加载前的轮廓。选错风格比没有动画更尴尬:给一个严肃的医疗后台配弹跳气泡,用户体验会非常跳戏。
我收藏的这150款里,按实现成本大致能分成三档:最基础的只用单个元素加一两个 keyframes,适合按钮内部的小菊花;中档的需要三五个子元素配合不同延迟,适合页面级loading;高档的会结合裁剪、滤镜、3D变换甚至两步动画串联,能承载品牌感很强的开场动效。整理这批素材时,我给自己定了一个原则:不只看“好看不好看”,还要看“实现方式能不能让我说得出口”。如果一个动画效果非常炫,但代码写得又长又绕,放到团队项目里别人接手困难,我会把它归入“仅欣赏”而不是“可复用”。
1.2 150款的真正价值:建立选型直觉
收藏1000个资源不如真正理解100个的实现思路。前面说“150款不是收藏数量的炫耀”,是因为当你把这么多样本摆在一起横向对比,会自然形成一套自己的选型直觉。
举例来说,看多了你会发现:转圈类loading中,border实现的双半圆环比伪元素拼出的圆环更容易控制粗细和颜色;点阵类loading中,用“透明度+延迟”实现的呼吸效果,比“位移+透明度”同时变化的效果更省GPU资源;骨架屏类loading中,用background-position做扫光,比逐帧替换背景图性能好一个量级。这些判断如果只盯着三五个轮子看,是永远总结不出来的。
另一个隐藏价值是版权和可信度。很多现成的loading仓库要么体积过大,要么License不明确,而自己维护一个经过筛选的CSS组件集合,校验来源、裁剪样式、统一命名,放到内部项目里完全没有后顾之忧。150款的规模刚好能覆盖绝大多数业务场景,也不至于多到维护不过来。
1.3 这份合集适合谁
说实话,如果你只想要一个能跑的loading效果,去CodePen搜“spinner”就够了。但如果你需要的是批量化接入、风格统一、可复用可解释的方案,那么理解这批动画的构成规律会更值钱。
适合三类人:第一类是前端工程师,尤其要做中后台系统、H5活动页、小程序组件,需要根据业务快速切换loading形态;第二类是UI/UX设计师,需要验证自己设计稿里的loading动效有没有更好的实现方式,避免“设计很丰满、CSS很骨感”;第三类是刚接触CSS3动画的新手,从这150款里挑几个经典开始拆解,比啃整本CSS书来得快。说白了,这是一篇“看着炫,拆开更香”的源码级解读。
2. 拆开看看:一款CSS3 loading动画到底由什么组成
2.1 静态骨架:一个容器和若干子元素
所有CSS loading动画,无论看起来多复杂,静态结构都离不开“一个容器 + 若干可复位的子元素”。容器负责定位和尺寸约束,子元素负责被动画属性驱动。经典的点阵loading,HTML通常是3到5个span;转圈loading,往往只需要div加上border边框;波浪loading,则可能需要多个块级元素叠在不同高度上。
我建议把“子元素数量”当作一个重要的选型指标。元素越多,视觉越丰富,但DOM开销和CSS代码量也会上升,在低端机上更容易出现卡顿。150款素材里,单一元素能做到惊艳的是少数,最典型的做法是用border-radius和transform组合出各类形态。如果你想在项目里减少DOM嵌套,优先挑这类“单节点方案”。
2.2 动效实现:animation 与 keyframes 的配合
核心代码永远是这两条:@keyframes定义关键帧,animation属性把时间轴挂到元素上。一个旋转loading的CSS可能短到十行以内:
.spinner { width: 40px; height: 40px; border: 4px solid rgba(0, 0, 0, 0.1); border-top-color: #3498db; border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } }很多新手以为keyframes必须写大段百分比,其实loading场景里,from和to两个关键帧往往就够。150款动画里真正迷人的部分,更多是多个元素设置了不同的animation-delay,让它们依次启动,形成视觉上的“波浪”或“错落”效果。比如三颗小球依次弹跳,就是给第2、第3颗球分别加上0.15s和0.3s的延迟。
2.3 形变转换:transform 中 rotate / scale / translate 怎么用
CSS3里最常用的三个形变函数,恰好对应三种最主流的loading形态。
rotate:转圈类。适合表达“处理中”“刷新中”。性能消耗低,因为它能命中GPU合成层。
scale:缩放类。适合表达“加载占比”“进度反馈”。CSS3的scale缩放方案在loading里最常见的用法是让圆形区域像呼吸一样由小到大、再由大到小。注意缩放最好不要单独作用于整个容器,否则容器里的文字会跟着一起变形,正确做法是把缩放作用于内部装饰层。
translate:位移类。适合模拟小球碰撞、字母跳动、进度条滑动。translate和opacity组合出现的概率非常高,因为位移结合透明度淡入淡出,会让元素移动不生硬。
这三类形变可以任意组合,比如“位移+缩放”“旋转+透明度渐变”。写代码时有个经验:loading动画里尽量只变化transform和opacity,不要动width、height、margin这些布局属性。前者走合成器,后者会强制重排,性能差距在低端手机上能差出十倍。
2.4 缓动与节奏:easing 决定“手感”
同样30帧的动画,linear和cubic-bezier呈现出的“手感”完全不同。线性旋转适合雷达、齿轮这类匀速机械感;弹跳小球则适合cubic-bezier(0.36, 0.07, 0.19, 0.97)这类先快后慢曲线;呼吸缩放更适合ease-in-out,让节点在两头减速。
150款素材中,有相当一部分的“高级感”就来自缓动。仔细观察会发现,很多炫酷效果的keyframes其实很普通,只是中间巧妙设置了一个反向关键帧。例如:
@keyframes pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(0.85); opacity: 0.5; } }这种“两端相同、中间不同”的写法配合ease-in-out,会让整个动画循环得特别顺。做loading选缓动时,我建议不要直接套默认值,依次试一下linear、ease-in-out、cubic-bezier三档,只用5分钟,效果差别非常明显。
3. 150款loading的分类选型,别再大海捞针
3.1 按视觉形态分:圆环、点阵、骨架屏、文字、混合
整理这批素材时,我按视觉形态把它们分成了五类,分类表如下:
| 形态 | 代表实现 | 适用场景 | 性能 | 难度 |
|---|---|---|---|---|
| 圆环类 | border圆环、SVG circle描边 | 全局加载、按钮内嵌 | 高 | 低 |
| 点阵类 | 多个点缩放/透明度错落 | 卡片局部加载 | 中 | 低 |
| 骨架屏 | 灰色背景渐变扫光 | 列表、图片占位 | 中 | 中 |
| 文字类 | 字母依次位移/翻转 | 品牌加载页 | 高 | 中 |
| 混合类 | 多个动态元素组合 | 引导页、首屏动画 | 低 | 中高 |
这种分类的价值在于你不需要在150个文件里挨个翻,看到“页面级、品牌感、需要撑住首屏”这些关键词,就直接定位到混合类和文字类;看到“接口请求按钮、表单提交中”就只看圆环类。我做内部组件库时,也按这套分类建目录,后续任何人进来都能快速找到需要的loading。
3.2 按使用场景选型:首屏、按钮、局部容器、图片占位
同一个loading放在不同位置,观感完全不同。选型时先问自己在哪个场景,而不是先看动画好不好看。
首屏加载:空间大、展示时间长,适合骨架屏或带品牌标识的混合动效。这类动画不能太闹,尽量保持页面整体色调统一,背景用浅色渐变,动画集中在视觉焦点。
按钮加载:空间很小,只允许一两秒内的微动效,圆环类最合适。这时有个细节:按钮里的loading必须和文字文案互斥,点击后按钮应进入disabled状态,把原来的文字替换成loading动画,尺寸控制在按钮高度的60%以内。我一般用CSS变量区分三种按钮尺寸:
.btn .spinner { width: calc(var(--btn-height) * 0.4); height: calc(var(--btn-height) * 0.4); border-width: calc(var(--btn-height) * 0.05); }局部容器:比如表格刷新、面板重载,点阵类和细圆环类都不错。不建议用大骨架屏,那样会让局部区域看起来像整页白屏。图片占位:骨架屏是主流方案,因为它能预告内容的轮廓,减少用户等待的焦虑感。
3.3 配速与配色:经验值直接抄
先给一组我多年实际项目中验证过的参数,直接抄不会有大的偏差。
| 项目 | 推荐值 | 理由 |
|---|---|---|
| 单圈旋转时长 | 0.6s - 1s | 低于0.5s显急促,高于1.2s显迟钝 |
| 点阵错落延迟 | 0.1s - 0.2s | 低于0.1s像一起动,高于0.25s像排队 |
| loading主色 | 品牌色或灰色系 | 不要用高饱和对比色作为主体 |
| 背景遮罩透明度 | rgba(0,0,0,0.35) 左右 | 太高压抑,太低看不到loading |
| 动画常驻时长 | 3s内结束最好 | 超过3s需要补充进度文字或重试入口 |
配色方面,千万别让整个loading由纯黑和大红组成,那会让用户觉得页面报错了。150款素材里面视觉最舒服的那批基本都符合“一个亮色点 + 若干浅色装饰”的规律。想做出质感,把主色饱和度降低5%-10%,页面会立刻温和很多。
4. 把150款变成一套可切换的组件
4.1 目录组织与命名规范
有了一堆CSS文件,直接扔到项目里早晚会乱。我常用的组织方式是按“形态-名称”命名目录:
loading-animations/ ├── spinner-ring/ │ ├── ring-1.html │ ├── ring-1.css │ ├── ring-2.html │ └── ring-2.css ├── dots/ │ ├── dots-1.html │ ├── dots-1.css │ └── ... ├── skeleton/ ├── text/ └── mixed/每个动画的HTML和CSS尽量独立成文件。这样既方便预览,也方便后续转成组件。如果做成组件库,我建议暴露一个统一的注册表:
export const loadingRegistry = { ring1: { html: '...', css: '...' }, dots1: { html: '...', css: '...' }, skeletonCard: { html: '...', css: '...' }, };注册表的好处是组件可以按key加载对应的模板和样式,实现真正的按需引用,而不是把所有动画全量打进bundle。
4.2 统一容器:HTML结构如何承载不同动画
无论动画内部长什么样,对外暴露的统一结构建议做成这样:
<div class="ui-loading">.ui-loading__inner { width: var(--ui-loading-size); height: var(--ui-loading-size); position: relative; display: inline-block; }这样无论内部是圆环、点阵还是文字,尺寸都受同一个CSS变量控制。vBird里看到的那些“只换动画名,其他不变”的组件,原理就是这个。
4.3 CSS变量做主题化配置
主题化是组件能不能推广开的关键。150款动画如果要手动改颜色,人会疯。用CSS变量可以一次性搞定:
:root { --loading-primary: #3498db; --loading-secondary: rgba(52, 152, 219, 0.2); --loading-bg: rgba(255, 255, 255, 0.9); --loading-speed: 0.8s; } .ui-loading__inner { border-top-color: var(--loading-primary); border-color: var(--loading-secondary); }动画中的每一个颜色、时长、尺寸都尽量抽成变量,后面你会感谢自己。比如项目A要求loading用品牌绿色,项目B要求Loading用天空蓝,只需要在入口处改两个变量,组件代码一行不动。同理,速度变量可以直接控制用户感知的“轻快/沉稳”,深色模式还能通过变量的覆盖自动切换。
4.4 按需加载与动画状态管理
做单页应用时,全量引入150个动画会让JS体积白白增加不少。好的做法是:所有loading组件懒加载,只在某个动画被使用时才注入对应CSS。
现代框架里,最容易落地的是CSS Module或者CSS-in-JS的按需方案。以React为例,可以封装一个组件,根据variant动态import:
function Loading({ variant = 'ring1', size = 32 }) { const [style, setStyle] = useState(null); useEffect(() => { import(`../loading-styles/${variant}.css`).then(() => setStyle(true)); }, [variant]); return <div className="ui-loading" style={{ width: size, height: size }}>{/* ... */}</div>; }状态管理还要考虑一件事:同一个页面上多个loading同时出现时,不能让所有动画都从零开始跑。可以用一个全局的“loading上下文”记录当前页面的加载状态,当切换路由或关闭弹窗时,统一移除挂载节点并清除动画class,避免僵尸动画一直空转。这个小细节很多组件库都没处理好,我自己踩过一次——弹窗关了,背后loading还在转,排查半天才发现是忘记清定时器。
5. 接入实战:从素材到生产环境的常见问题
5.1 动画不动的诊断清单
收藏的150款动画往项目里搬时,“动画不动”是最高频问题。我列了一份排查清单,按顺序过一遍能解决九成情况:
- 检查元素是否能被CSS选中:类名冲突、选择器层级、CSS文件有没有被正确引入。
- 检查元素尺寸是否为0:border实现了圆环loading,若容器宽高是0,边框不显示;把
width和height临时加到100px验证。 - 检查transform是否被覆盖:项目里通用样式如果写了
transform: translateZ(0)提性能,可能会覆盖关键帧里的transform: rotate(360deg)。解决方案是用animation-composition或把旋转挂到子元素上。 - 检查prefers-reduced-motion:一些系统开启“减少动态效果”后,浏览器会限制CSS动画,可以用代码兜底显示静态loading。
- 检查浏览器兼容:低版本内核支持度有限,后面专门列兼容表。
5.2 页面卡顿与GPU加速
Loading动画虽然简单,但如果是全局弹层、满屏刷新,依然可能拖垮低端机。卡顿通常不是单个动画性能差,而是同时有过多元素在做复合动画。
排查手段有两个:打开DevTools的Performance面板录制一段loading,看动画片段里的Scripting和Rendering用时;再开启Rendering面板的“Layer borders”,观察有没有大量黄色图层。出现卡顿后,优先做三件事:
- 把width、height、margin等布局属性替换成transform中的scale和translate。
- 给动画元素加上
will-change: transform(用完记得移除,否则内存占用会一直很高)。 - 减少同时变化的元素数量,点阵loading从8个点降到5个点,视觉上几乎无差别。
5.3 与其他CSS框架的冲突处理
很多项目配套了Tailwind、Element、Antd,动画不动可能是因为框架级样式影响了子元素的定位方式。最常见的冲突有四个:
| 冲突来源 | 现象 | 解决方案 |
|---|---|---|
| reset.css覆盖border样式 | border圆环完全消失 | 在组件类名下重写border |
| flex导致容器尺寸异常 | 点阵全部挤到一行 | 给inner加固定宽高 |
| 全局transition干扰 | 动画出现延迟或过渡 | 对inner单独设置transition: none |
| 全局transform属性 | 关键帧rotate被覆盖 | 给动画元素加更具体的选择器 |
如果项目UI框架自带transition且作用于所有后代元素,我习惯在loading容器上加一句:
.ui-loading__inner, .ui-loading__inner * { transition: none !important; animation: none !important; }这里不是推荐滥用!important,但loading动画往往需要在框架重压下“保命”,这对生产环境是必要的兜底。
5.4 兼容性:一张表直接对照
CSS3动画主流浏览器支持已经很成熟,但是在老项目、特定浏览器环境里,还是要先看兼容基线:
| 特性 | Chrome | Firefox | Safari | IE/Edge旧内核 |
|---|---|---|---|---|
| transform | 支持 | 支持 | 支持 | IE10+ |
| animation | 支持 | 支持 | 支持 | IE10+ |
| CSS变量 | 支持 | 支持 | 支持 9.1+ | 不支持 |
| prefers-reduced-motion | 支持 | 支持 | 支持 | 不支持 |
| backdrop-filter | 部分 | 支持 | 支持 | 不支持 |
如果你的产品需要兼容IE11,CSS变量方案就需要换成SCSS变量编译或者直接在组件里写死颜色。另外,animation-delay负数是一个很好用的技巧:把delay设为负值会让动画直接跳到时间轴中间,多个loading同时引入时它能让动画看起来“已经在跑”,而不是齐刷刷从第一帧启动。这点在首屏多组件加载时非常实用。
6. 三个经典loading从零手写
6.1 旋转圆环:最快落地的一款
单元素实现,前面已经贴过基础版。这里我给一个带磨砂遮罩的完整版,适合页面级全局loading:
<div class="loading-mask"> <div class="spinner-ring"></div> </div>.loading-mask { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(255, 255, 255, 0.75); backdrop-filter: blur(4px); z-index: 9999; } .spinner-ring { width: 48px; height: 48px; border: 5px solid rgba(52, 152, 219, 0.15); border-top-color: rgba(52, 152, 219, 0.9); border-radius: 50%; animation: ring-rotate 0.8s linear infinite; } @keyframes ring-rotate { to { transform: rotate(360deg); } }有几个细节值得展开。用inset: 0代替top/left/right/bottom: 0能让代码更整洁;border-top-color单独设置彩色比其他三条边框全彩更耐看,视觉重心更集中;animation-delay不需要设置,因为旋转是连续循环的,任何时候插入都是“已在转”的状态。如果想做双色环,再加一层伪元素,错开旋转方向即可。
6.2 三点呼吸:局部加载最常用
按钮局部加载建议用点阵。以三个点为例,HTML结构保持三个span,CSS里利用nth-child控制延迟:
<div class="dots-loading"> <span></span> <span></span> <span></span> </div>.dots-loading { display: flex; gap: 6px; } .dots-loading span { width: 8px; height: 8px; border-radius: 50%; background: var(--loading-primary, #3498db); animation: dot-breathe 1.2s ease-in-out infinite; } .dots-loading span:nth-child(2) { animation-delay: 0.2s; } .dots-loading span:nth-child(3) { animation-delay: 0.4s; } @keyframes dot-breathe { 0%, 100% { transform: scale(0.8); opacity: 0.5; } 50% { transform: scale(1.1); opacity: 1; } }这里使用的“缩放+透明度”双属性组合,比单纯位移更柔和。gap设置成6px左右,点之间不会拥挤。如果你在按钮里用,记得把span尺寸改成按钮高度的40%,并把间距同步缩小,否则三个点会把按钮文案挤爆。
6.3 骨架屏:从列表到卡片都能套
骨架屏不属于典型的旋转loading,但用户体验最佳。以一张商品卡片为例:
<div class="skeleton-card"> <div class="skeleton-block image"></div> <div class="skeleton-line title"></div> <div class="skeleton-line price"></div> </div>.skeleton-card { padding: 16px; border-radius: 12px; background: #fff; } .skeleton-block, .skeleton-line { background: linear-gradient(90deg, #eee 25%, #ddd 37%, #eee 63%); background-size: 400% 100%; animation: skeleton-sweep 1.4s ease infinite; } .skeleton-block.image { height: 120px; border-radius: 8px; margin-bottom: 12px; } .skeleton-line.title { height: 16px; border-radius: 4px; margin-bottom: 10px; } .skeleton-line.price { width: 80px; height: 16px; border-radius: 4px; } @keyframes skeleton-sweep { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }核心思路是让渐变背景在元素内部循环移动,模拟一道光扫过占位区域。这里用background-size: 400%保证扫光效果有足够的移动空间。骨架屏的动画时间可以比旋转类慢一些,1.3s到1.6s都不错,扫太快会让人分心。另外骨架屏最好搭配一点微弱的整体透明度变化,避免在暗色模式下显得刺眼。
7. 我的真实体会和差异化建议
7.1 收藏150款之后怎么消化
收藏这件事本身没有价值,消化才有。面对150款动画,我的方法是每周抽出半小时做“动画考古”:选三个动画,拆解它们的关键帧,尝试改一个参数看有什么变化。这样持续一个月,你会形成对动画节奏非常敏锐的手感。之后你再看到任何loading,会下意识猜它用了几个关键帧、每个子元素延迟多少,而不是只会说“好看”。
另外,我强烈建议每个开发者都维护一个自己的“小样本库”。不需要150款那么多,把自己最常用的20款CSS3 loading放进去,加上注释:为什么用这个、适合什么场景、有没有性能坑。这比收藏夹里堆几百个链接有用得多。面试或评审时,这也是一个很好的差异化亮点——大多数人都只是“用过”,你能讲清楚“为什么”。
7.2 做loading时的心态与设计原则
最后分享一个我踩过多次坑后总结的原则:loading动画的本质是“让等待变短”。它不是炫技舞台,不是把用户注意力全吸走的设计彩蛋。好的loading应该做到存在感适中、风格统一、对后续内容有所呼应。150款素材每款都不是终点,而是起点——当你理解了它们的构成和取舍,你完全能在此基础上改出自己的品牌版本,而不再需要满世界找素材。
我个人在实际项目里最常用的反而是最简单的圆环和骨架屏,复杂动效一年用不到两三次。但正因为看过了那150款,我才能确定简单的就是最优解,这就是大规模刷素材给我带来的最大底气。如果你也收藏了一堆“炫酷收藏夹”,别让它们吃灰,抽个下午,逐个打开、拆解、跑一遍,你会收获比“会抄代码”多得多的东西。