ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

17个纯CSS加载动画与进度条:核心机制、性能优化与实战代码

17个纯CSS加载动画与进度条:核心机制、性能优化与实战代码 1. 为什么加载与进度动画值得单独攒成一套收藏做前端这些年我删过无数个收藏夹唯独这一类没舍得删加载动画与进度条。原因很朴素——它们是项目里出现频率最高、复用成本最低、但视觉效果差距最悬殊的一类 UI 组件。同样一个数据请求中的场景有人放一个灰色转圈有人放一组流畅跳动的点阵用户对产品档次的判断就在这零点几秒里完成了。这份收藏是我两个晚上熬出来的一共 17 个效果覆盖了 CSS 加载动画的主流实现路径纯transform旋转类、多元素错帧跳动类、SVGstroke-dasharray描边类、conic-gradient环形进度类以及几条数值驱动的进度条方案。它们全部只用 HTML CSS部分需要 JS 参与数值更新不依赖任何第三方动画库。适合刚学完 CSS 基础想练手的同学也适合有一定经验、想给项目补一套轻量加载组件的开发者。我选素材时有三个硬标准先说清楚免得你觉得这 17 个是随便凑的数能一行不写 JS 就不写。加载动画本身就是临时占位引入 JS 反而增加主线程负担还可能和业务请求抢资源。只动transform和opacity。这两个属性走合成层不触发重排重绘低端机上掉帧概率最小。凡是靠改width、top、left做动画的方案我一个都没收。可拆卸。每个效果我都能独立抽成一个组件、一段样式不会牵一发动全身。提示收藏这类素材的正确姿势不是存起来就完事而是把每个效果的动画函数曲线和时间参数抄进自己的笔记。真正能复用的不是颜色和尺寸而是那串cubic-bezier和时间节奏。接下来我会先讲底层机制再拆具体效果最后讲进度条的数值驱动和避坑。内容偏实操代码都经过我本机验证你复制过去改改尺寸配色就能用。1.1 加载动画和进度条其实是两个不同的心理问题很多人把这两者混为一谈都叫loading但它们在用户心理层面解决的是完全不同的焦虑。加载动画indeterminate解决的是系统还活着吗。用户点了一个按钮页面没反应三秒之后他会开始怀疑是不是卡死了然后疯狂重复点击。加载动画的作用就是持续释放我在工作的信号。它的核心指标只有一个有没有在动。所以这类动画不需要精确甚至可以故意做得不精确因为一旦你给了精确进度反而会暴露真实耗时。进度条determinate解决的是还要等多久。它必须给出一个可信的数字或比例用户才能决定是继续等还是去倒杯水。它的核心指标是进度与实际加载的匹配度。我见过太多项目用假进度条——加载到 90% 卡住不动这是最伤用户信任的做法比不放进度条还糟糕。理解这个区别你在选型的时候就不会跑偏。请求接口用前者文件上传、资源预载、多步骤表单用后者。下面这套 17 个效果我在分类的时候也是按这个逻辑切的。1.2 17 个效果的整体分类与选型思路为了让你有个全局感我先把它们按实现原理分个组。这张表我建议你先扫一眼后面拆解时对照着看遇到需要哪一类直接跳过去。分组数量代表效果核心技术适用场景形变旋转类6双环旋转、方块翻转、点阵脉动keyframestransform局部按钮、卡片内加载描边线条类6环形描边、虚线追逐、描边扫光SVG stroke-dasharray全屏等待、品牌感强的场景数值进度类5条纹进度、环形进度、分段步进CSS 变量 conic-gradient上传、下载、流程进度这个分类不是随便分的它对应着三种不同的渲染路径。形变旋转类靠 GPU 合成层最轻描边线条类依赖 SVG 路径计算次之数值进度类需要 JS 参与状态同步最重。你项目里如果用得密集优先选前两类。选型上我还有一个经验同一产品内不要混用超过两种动画语言。我见过一个后台系统列表加载用圆环、按钮加载用三点跳、上传用条纹条三个地方三种风格用起来像三家公司的产品拼在一起。你要么全用圆环系要么全用线性系统一比炫酷重要得多。2. 动手前必须理清的 CSS 动画底层机制开始拆代码之前有几个机制必须掰开揉碎讲。跳过这段直接复制代码你大概率会遇到为什么我的动画卡顿为什么改个属性整个动画重来一遍这种问题然后回来翻文档浪费的时间比现在读这一节多得多。2.1 transition 与 animation 的分工别搞混新手最容易犯的错是拿transition去做循环动画。这两者的定位完全不同transition是状态过渡它需要两个状态起始态和目标态由某个属性变化触发比如 hover、class 切换。它天然只有一次做完就停。animation是关键帧动画它自己定义整个时间轴上的每一段可以循环、可以反向、可以延迟不依赖状态变化。加载动画是无限循环的所以必须是animation。那transition在哪用在进度条上——当你用 JS 把--progress从 30 改成 60宽度从 30% 变到 60%这个过程用transition来做平滑补间比手写关键帧自然得多。我举个实际写过的例子进度条部分.progress-bar { width: calc(var(--progress) * 1%); transition: width 0.4s cubic-bezier(0.22, 0.61, 0.36, 1); }注意这里的cubic-bezier(0.22, 0.61, 0.36, 1)这是一个先快后慢的缓出曲线。为什么不用默认的ease因为默认ease在中段加速感太强视觉上会有点冲而进度条要的是一种沉稳推进的感觉缓出更合适。2.2 关键帧百分比与时间函数怎么配合keyframes里的百分比是相对整个动画周期的不是绝对时间。这是理解动画节奏的基础。举个例子animation-duration是 1.2s那么关键帧里的 50% 就是 0.6s 那一刻。这就带来一个实用技巧想让某段动作快不是去改百分比而是改时间函数。比如keyframes pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.35); opacity: 0.55; } } .dot { animation: pulse 1.4s ease-in-out infinite; }这里 0% 和 50% 之间是放大过程ease-in-out让它在起点和终点都变慢、中间加速出来的效果就是呼吸感。如果我把它换成linear缩放会变成机械的匀速运动看起来像机器在推拉而不是呼吸。我实测过1.2s 到 1.6s 是呼吸类动画最舒服的区间。低于 1s 会显得焦躁像在催用户高于 2s 又像卡住了用户会以为页面出问题。这个区间你也可以拿去用在按钮的加载态上。2.3 只动 transform 和 opacity 的硬性理由这一条我要强调三遍加载动画只动transform和opacity只动transform和opacity。浏览器渲染一帧要经过布局Layout、绘制Paint、合成Composite三个阶段。你改width、height、margin、top、left、font-size这些属性会触发 Layout浏览器得重新算一遍所有元素的位置代价极高。改background-color、box-shadow这些会触发 Paint代价中等。只有transform和opacity可以只走 Composite在 GPU 上完成代价极低。我做过的对比测试同样一个旋转动画用rotate()在低端安卓机上稳定 60 帧用left位移实现同样的视觉帧率掉到 35 左右而且手机明显发热。还有个坑box-shadow做脉动效果。看起来很炫但因为每帧都要重绘阴影CPU 占用会飙升。如果你非要用发光脉动正确做法是把光做成一个独立的伪元素用opacity去控制它的显隐而不是动box-shadow本身。注意will-change: transform不是万金油。它会让浏览器提前把元素提升为独立图层但图层多了显存吃紧。加载动画上写一次就够了别在几十个点阵元素上都写反而更慢。3. 17 个加载动画的逐个拆解与关键代码这一节我按分组拆。每个效果我会给核心思路 关键代码 我改过的参数。你把代码复制进一个空 HTML 页面就能跑不用装任何环境。3.1 前 6 个形变旋转类轻量首选这一类全部靠transform的rotate、scale、translate打天下是加载动画里性能最好的一档我优先推荐用在按钮内、卡片内这些出现频率高的位置。第 1 个经典双环反向旋转。两个圆环反向转中间留空隙。核心是border的四边颜色不同然后整体旋转.spinner-dual { width: 40px; height: 40px; border: 3px solid transparent; border-top-color: #4c8bf5; border-radius: 50%; animation: spin 0.9s linear infinite; } .spinner-dual::after { content: ; position: absolute; inset: 5px; border: 3px solid transparent; border-bottom-color: #f5a623; border-radius: 50%; animation: spin 0.9s linear infinite reverse; } keyframes spin { to { transform: rotate(360deg); } }inset: 5px是内圈的定位比写top/left/right/bottom简洁。反向用reverse关键字不重新写关键帧。0.9s 这个数值是我反复调出来的快了像在抖慢了像卡住。第 2 个脉冲圆点。单个点在缩放和透明之间来回适合做正在思考的状态。代码就是上面提过的pulse关键帧重点在于scale不要超过 1.4否则视觉上会撑破容器。第 3 个三点跳动。三个点依次translateY上跳关键在错帧.dot { animation: bounce 1.2s infinite ease-in-out; } .dot:nth-child(2) { animation-delay: 0.15s; } .dot:nth-child(3) { animation-delay: 0.3s; } keyframes bounce { 0%, 60%, 100% { transform: translateY(0); } 30% { transform: translateY(-8px); } }60%到100%保持不动是为了给动作留出落回后静止的停顿这个停顿让整组动作有了节奏感。如果三个点均匀上跳看起来就会很呆。第 4 个方块翻转。一个方块绕 X 轴或 Y 轴翻转.cube { animation: flip 1.8s infinite cubic-bezier(0.6, 0.05, 0.15, 0.95); } keyframes flip { 0% { transform: perspective(120px) rotateX(0) rotateY(0); } 50% { transform: perspective(120px) rotateX(-180deg) rotateY(0); } 100% { transform: perspective(120px) rotateX(-180deg) rotateY(-180deg); } }perspective()一定要加否则翻转看起来是扁的没有立体感。这个曲线我换过好几版最后这个cubic-bezier出来的翻页感最像真实纸张。第 5 个波浪柱状条。五根竖条依次伸缩像音浪。写法和三点跳动几乎一样把translateY换成scaleY然后把每根柱子的animation-delay依次加 0.1s。这里有个细节transform-origin要设成bottom不然柱子会从中间往两头撑。第 6 个九宫格脉动。3×3 的圆点每个点的延迟按行列位置算出形成斜向传播的波纹。延迟值我用的是(row col) * 0.08s出来的传播是对角线方向比按行传播更有层次。这六个里我个人最常用的是第一个和第三个。第一个稳第二个活泼覆盖了大部分场景。3.2 中间 6 个描边线条类视觉质感更强这一组开始上 SVG。SVG 的优势是路径任意能做出圆形之外的形状而且描边可以精确控制。代价是需要一点点路径知识。第 7 个SVG 环形描边旋转。这是最经典的一种用stroke-dasharray和stroke-dashoffset配合.ring-path { fill: none; stroke: #4c8bf5; stroke-width: 4; stroke-linecap: round; stroke-dasharray: 126 188; /* 126 是可见长度188 是周长补足 */ animation: dash 1.5s ease-in-out infinite; } keyframes dash { 0% { stroke-dashoffset: 0; } 50% { stroke-dasharray: 40 314; stroke-dashoffset: -60; } 100% { stroke-dasharray: 126 188; stroke-dashoffset: -314; } }这里的关键是周长计算。假设圆半径 30周长就是2π×30 ≈ 188.5。stroke-dasharray: 126 188表示 126 实线、188 空白合起来刚好覆盖一圈多一点所以看起来是固定的 2/3 圆弧在转。50% 那里我把实线长度压到 40是为了模拟拉伸的感觉。stroke-linecap: round让线端变圆这个小细节极大提升质感很多教程漏了。第 8 个虚线追逐。一条虚线环绕跑动。核心是stroke-dasharray把它切成均匀的小段然后动stroke-dashoffsetkeyframes chasing { to { stroke-dashoffset: -100; } }stroke-dashoffset是负值的时候虚线朝运动方向流动。这个属性单位是 SVG 用户单位不是像素所以要注意 viewBox 的缩放。第 9 个心电图脉冲线。一条折线在横向平移看起来像示波器。用translateX配合一个宽于容器的路径平移到左端后瞬间复位keyframes ecg { 0% { transform: translateX(0); } 100% { transform: translateX(-50%); } }这里我用-50%而不是-100%是因为路径画了两遍首尾相接只要平移一半就完成一个循环接缝完全看不出来。这是做无缝滚动的一个通用技巧值得记住。第 10 个描边文字扫光。文字本身是空心描边效果用-webkit-text-stroke或者text-shadow叠多层实现。再用一个渐变遮罩扫过去keyframes shine { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }背景渐变用linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent)配合background-clip: text就能让光只在文字区域内显示。第 11 个删除线扫过。这个和上面的思路一致只是把渐变扫过换成一条横线从左到右划过去。实现方式是在文本元素上放一个伪元素当作线用transform: scaleX()从 0 拉到 1.strike::after { content: ; position: absolute; left: 0; top: 50%; height: 2px; width: 100%; background: currentColor; transform: scaleX(0); transform-origin: left; animation: strike 1.6s ease-in-out infinite; } keyframes strike { 0% { transform: scaleX(0); } 50%, 100% { transform: scaleX(1); } }用scaleX而不是width原因还是性能。这条线如果用在正在删除中的交互反馈上效果相当自然。第 12 个线条无限拉伸的进度轨道。一根本体固定的轨道上有一段高亮条来回跑。这种叫 indeterminate 进度条最适合用在总数未知的场景keyframes slide { 0% { left: -35%; right: 100%; } 60% { left: 100%; right: -90%; } 100% { left: 100%; right: -90%; } }这里的left和right同时变化效果是滑块被拉长再收缩比单纯平移更有动感。不过注意这个方案动了left/right会有重排开销。如果对性能极敏感可以改成用一个两倍宽的条做translateX效果接近。3.3 后 5 个数值进度类必须和 JS 配合这一组是真正的进度条需要外部数据驱动。核心是把进度值写成 CSS 自定义属性样式里只读这个变量。第 13 个条纹滚动进度条。线性渐变画斜条纹然后用background-position让它滚动.bar-striped { background-image: repeating-linear-gradient( 45deg, rgba(255,255,255,0.25) 0 10px, transparent 10px 20px ); animation: stripes 1s linear infinite; } keyframes stripes { to { background-position: 28px 0; } }位移量 28px 是怎么来的45 度斜条纹条纹周期是 20px横向位移一个周期需要20 / sin(45°) ≈ 28.3px。取 28 或者用1.414 × 20都行只要保证循环时图案能接上否则会看到跳一下。第 14 个液态波浪进度条。用一个圆角矩形做波浪border-radius给到 45%整体旋转看起来就是在晃动的液体。这个效果配合较低的透明度质感非常好.wave { border-radius: 45%; animation: rotateWave 12s linear infinite; } keyframes rotateWave { to { transform: rotate(360deg); } }转速不能快12s 一圈比较自然。快了就像搅拌机。第 15 个conic-gradient环形进度。这是近几年最省事的环形方案不需要 SVG.ring { background: conic-gradient(#4c8bf5 calc(var(--p) * 1%), #e8eaf0 0); border-radius: 50%; mask: radial-gradient(circle, transparent 62%, #000 63%); }conic-gradient从 12 点方向开始按角度铺色calc(var(--p) * 1%)就是进度百分比。mask用径向渐变挖掉中间形成圆环。这个写法在主流浏览器上支持已经很好比 SVG 方案少写一半代码。第 16 个分段步进进度条。把总进度拆成 N 段小格子依次点亮。实现方式是把每个格子做成一个元素用animation-delay依次亮起或者用 JS 根据当前段数给格子加 class。前者适合自动播放型后者适合真实进度型。第 17 个无限滚动跑马灯式加载条。严格说它不是进度条而是介于两者之间的过渡型。一条渐变色带在轨道里匀速跑动用来填补进度待定的空档期。它和上面第 12 个的区别在于这个更强调视觉上的连续感常用于整页预载页面。4. 进度条的数值驱动与结构设计光有样式不够进度条的核心在于数值怎么进来、怎么平滑、怎么不出错。这一节讲工程侧的东西。4.1 结构选型div 方案 vs SVG 方案 vs conic-gradient 方案我做选型时按三个维度权衡代码量、可控精度、可访问性。方案代码量精度控制支持度推荐场景div width最少线性够用全支持横向条形进度SVG中等任意路径全支持环形、不规则形状conic-gradient少环形够用现代浏览器环形不要求老浏览器横向条我一律用 div CSS 变量简单到不能再简单。环形我优先conic-gradient只有在必须支持老旧环境时才退回 SVG。SVG 的额外成本是你要自己算周长、处理 viewBox 缩放代码一多容易出错。不管哪种方案我都建议把进度值统一收敛到一个 CSS 变量上比如--p范围 0 到 100。这样你在 JS 里只管改这一个值样式层的适配全在 CSS 里完成function setProgress(el, percent) { const p Math.max(0, Math.min(100, percent)); el.style.setProperty(--p, p.toFixed(1)); }Math.max/min这个钳位看着不起眼但非常有必要。我就遇到过因为后端返回了一个 105 的进度导致圆环过度绘制视觉上溢出容器排查了半小时。4.2 百分比平滑与假进度的正确打开方式真实进度经常是跳变的比如一次性返回 30%下一次直接到 80%直接设值会显得很突兀。这时候加一个过渡.progress-fill { transition: width 0.4s ease-out; }但过渡时间不能太长否则会出现进度条还在爬实际已经加载完了的错觉。0.3s 到 0.5s 是我常用的区间。至于假进度我的原则是宁可放 indeterminate 动画也不放一个会卡住的假百分比。如果真的需要一个心理安抚型的进度可以做一个渐近式假进度——从 0 快速涨到 60然后以很慢的速度继续往 90 爬永远不会到 100直到真实加载完成后瞬间补到 100let fake 0; const timer setInterval(() { fake (90 - fake) * 0.08; // 越接近 90 涨得越慢 setProgress(bar, fake); }, 300);(90 - fake) * 0.08这个公式是让增长速度随接近上限而衰减视觉上非常自然。加载完成时清掉定时器把值设成 100。4.3 尺寸与主题适配用 em 和 CSS 变量加载组件的一个隐藏需求是要能塞进任何尺寸的容器。按钮里的加载是 14px卡片里的是 40px全屏的是 80px。如果每个尺寸都写一份样式维护成本爆炸。我的做法是让所有尺寸基于一个变量.spinner { --size: 24px; --thickness: calc(var(--size) / 8); width: var(--size); height: var(--size); border-width: var(--thickness); }这样你只需要改--size一个值线宽自动跟着变。size / 8这个比例是我试出来的出来线宽视觉上刚好太细会看不清太粗会显得笨重。主题色也一样全部走currentColor。这样组件放在任何文字颜色的容器里都会自动和文字同色不用单独传颜色参数。这一个技巧能省掉你大量的样式覆盖代码。5. 性能、兼容与可访问性避坑清单前面讲的都是怎么做出来这一节讲怎么不出事。这部分内容网上讲得少但实际项目里最容易翻车。5.1 加载动画导致的卡顿排查思路加载动画出现在什么时刻恰恰是页面最忙的时候——在请求接口、在渲染大数据、在解析资源。这时候如果你的动画本身很重就会和主线程抢资源形成恶性循环越卡越动画掉帧越掉帧看起来越像死了。排查顺序我一般是这样走的打开浏览器性能面板录制 5 秒看是否有长任务。检查动画元素是否触发了 Layout看渲染面板里的紫色块。检查图层数量图层过多会吃显存尤其移动端。检查是否有大量元素同时执行animation。我踩过的最典型的一个坑一个九宫格加载动画我在 9 个点上同时写了animation: pulse 1.5s infinite看起来没问题但在低端机上帧率直接腰斩。后来改成只用一个父元素加animation9 个点靠transform继承和延迟错开性能立刻回来了。注意filter: blur()慎用在动画元素上。它是一个非常昂贵的属性每帧都要重新计算模糊哪怕只动opacity只要元素上有 blur合成成本都会高很多。要做模糊光晕用一张预先模糊好的图片代替。5.2 尊重系统的减少动态效果偏好这是一个很多人忽略但越来越重要的点。操作系统里有减少动态效果的设置用户开启后你的动画应该收敛。做得好的话这是产品成熟度的体现media (prefers-reduced-motion: reduce) { .spinner, .dots, .bar-striped { animation-duration: 2s; animation-iteration-count: 1; } }我一般不是直接关掉而是把动画周期拉长、取消无限循环。因为完全静止的加载状态会让用户以为卡住了同样体验不好。这个折中方案我用下来反馈最好。顺带说一句可访问性加载容器上加上rolestatus和aria-livepolite屏幕阅读器会在状态变化时播报。文本内容用aria-label给出比如正在加载已完成百分之六十。这些属性不占多少代码但用起来专业度差很多。5.3 常见问题速查表下面这张表是我这两晚整理时顺手记下来的都是实际会遇到的现象常见原因处理方式动画跑偏、位置跳动用了top/left做位移动画换成transform: translate旋转中心不对没设transform-origin显式指定中心点默认是元素中心动画结束瞬间闪回关键帧首尾状态不一致让 0% 和 100% 属性完全对齐圆环首尾接不上周长算错了按2πr重算或用浏览器逐帧核对进度条超过 100%后端数值异常JS 层做钳位别只靠 CSS高分屏上模糊用了 1px 以内的线宽线宽取整或用transform: scale动画在 Safari 上不动前缀或属性支持问题优先用transform避开新属性最后一条我要多说一句conic-gradient这类较新的特性用之前最好在目标环境的真机上过一遍。我有一次在开发机上跑得好好的到一台老平板上直接变成纯色块就是因为不支持。6. 两个晚上踩出来的细节和后续延展写到这里17 个效果和背后的机制都过了一遍。最后分享几个只有动手做才会发现的东西。关于配色我一开始特别喜欢用高饱和的蓝色觉得亮眼。做多了才发现加载动画的配色应该比主视觉低一档饱和度。因为它是持续运动的元素高饱和的循环运动会让人视觉疲劳用户盯着看十几秒会很难受。后来我统一改成主色降 20% 饱和度加一点点灰观感舒服太多。关于节奏同一个页面里如果同时出现多个加载动画它们的周期最好错开。我见过一个页面三个转圈同时转频率一致但相位一致看起来像三个同步的机器人非常机械。后来我给其中一个加了 0.3s 的随机延迟整体感觉马上就活了。关于缓存如果你把这些动画做成组件记得把关键帧抽到全局样式里。我遇到过同一个keyframes spin在六个组件里各写一遍编译后体积大了好几 KB虽然能被压缩工具去重但不如一开始就写在全局。后续这套东西还能怎么延展我最近在试两个方向。一个是把进度条的数值和 Web Animations API 结合用 JS 控制关键帧的播放进度做出可以暂停、倒放的进度反馈用在可撤销的批量操作上很有意思。另一个是把加载动画和骨架屏结合起来——加载初期用骨架屏占位超过 800ms 还没返回数据再在骨架屏上叠一层轻量的脉冲动画。这样既避免了一闪而过的动画闪烁又能在真正慢的时候给出等待反馈。这两个方向我都还在调等稳定了再单独整理一篇。眼下这 17 个效果你先把形变类和数值类的用起来覆盖日常开发九成场景足够了。
返回列表