简介:一套基于纯CSS3实现的Loading文字加载动效源码,面向前端开发者和网页设计人员,适用于页面加载、数据请求等待等需要明确的过渡场景。它不依赖JavaScript实现核心动画,而是利用CSS3的animation属性让文字产生上下滚动与字符拆分效果,从而缓解用户等待焦虑,同时降低页面性能压力。资源压缩包仅7KB,共7个文件,包括3个CSS样式文件、2个JS辅助文件、1个HTM页面和1个HTML页面,CSS部分负责动画定义与拆分样式,JS文件辅助完成文字拆分与交互控制,文件结构一目了然,方便直接套用或改造。目前已有281人学习下载。借助这份源码,可以系统理解animation-name、animation-duration、animation-timing-function、animation-delay、animation-direction、animation-iteration-count、animation-fill-mode和animation-play-state这八个关键子属性的协同方式,并学会借助splitting工具快速实现字符级动效;无论是入门CSS动画还是寻找轻量加载动效模板,都能从中获得可直接落地的代码与调参思路。
1. 纯 CSS3 Loading 文字动效:为什么几行样式表比一张 GIF 更值
做后台项目时,页面加载的时候总要给用户一个回应:转圈、进度条、或者一行“加载中”文字。纯 CSS3 Loading 文字加载动效就是针对后者的一种实现,不依赖任何动画库,不产生额外图片请求,文字内容、颜色、速度全部由 CSS 控制。一套做下来,体积也就几 KB,比 GIF 干净,比视频动效轻量,更关键的是它在任何分辨率下都不会糊,深色浅色主题都能自适应。
这套源码包的定位很实在:解决“加载提示太丑”的问题。无论你是做管理后台、移动端 H5,还是个人网站的落地页,只要需要用文字告诉用户“正在加载”,都能直接用。配合少量 JS 常用代码,可以把动效控制得非常精确——什么时候出现、什么时候消失、加载失败时显示什么文本,都由你说了算。下面我从原理拆到实现,把里面值得注意的边界和坑一个一个讲清楚。
2. 从 @keyframes 拆起:逐字显现、波纹穿透、遮罩扫过的实现与选型
2.1 一个动画的最小骨架:@keyframes 和 animation 属性组
所有 CSS3 文字动效的核心都绕不开@keyframes。你以为它只是“关键帧”,实际上它是一个微型状态机:定义动画从头到尾所有关键时刻的样式,浏览器按时间插值,自动补全中间帧。一个最简单不过的上下弹跳动画长这样:
@keyframes loading-bounce { 0% { transform: translateY(0); opacity: 0.4; } 30% { transform: translateY(-0.5em); opacity: 1; } 100% { transform: translateY(0); opacity: 0.4; } } .loading-text span { display: inline-block; animation: loading-bounce 1.2s ease-in-out infinite; }这里的逻辑很直白:0%是起点,文字在原位且半透明;30%是整个动画的最高点,文字上移0.5em且完全不透明;100%回到起点。注意我特意用了translateY而没用top,因为transform只触发合成器工作,不触发 layout 重排,这点在后面排查卡顿时会再提到。
animation是多个属性的简写,在这一行里同时定义了三样东西:动画名、时长、缓动函数。但真正让文字动效出彩的,是另外几个经常被忽略的属性。animation-delay控制每个字符的启动时间,把若干个字符的 delay 错开,就形成了“波浪”;animation-direction设为alternate时动画会在正放和倒放之间来回,适合做呼吸感;animation-fill-mode决定动画开始前和结束后用哪一帧的样式。这四个属性,加上iteration-count、play-state,基本就是所有 CSS 动效的完整工具箱。
2.2 逐字显现:用 delay 让每个字排队进场
逐字显现是最常见的 Loading 文字效果,做起来也最直观。把每个字符单独包一个span,给每个 span 设置递增的animation-delay,让它们按顺序做同一套动画。比如四个字“加载中”,第一个字延迟 0s,第二个延迟 0.15s,第三个延迟 0.3s,依次类推:
.typing-loader span { display: inline-block; opacity: 0; animation: char-in 1s ease forwards; } .typing-loader span:nth-child(1) { animation-delay: 0s; } .typing-loader span:nth-child(2) { animation-delay: 0.15s; } .typing-loader span:nth-child(3) { animation-delay: 0.3s; } .typing-loader span:nth-child(4) { animation-delay: 0.45s; } @keyframes char-in { 0% { opacity: 0; transform: translateY(0.3em); } 100% { opacity: 1; transform: translateY(0); } }这段代码里有个容易被忽略的细节:opacity: 0写在span上是“动画开始前”的样子,forwards则让动画结束后保留100%那帧的样式。如果忘了写forwards,动画播完会瞬间跳回透明状态,感官上就是文字闪一下没了,这类坑在现实中特别常见。
如果你的 Loading 文字不是固定四五个字,而是动态传入的,延迟不会写死,而是按序号计算。常见做法是在 JS 渲染字符时直接内联style="animation-delay: calc(0.15s * var(--i))",配合--i变量递减,代码整洁且不污染 CSS 文件。
2.3 遮罩扫过:不碰文字本身,用一层渐变盖上去
逐字显现适合“打字机”风格,但有些场景需要更强的视觉冲击,比如文字上加一道高光扫过去。这种效果的核心思路是:文字本身不动,动的是覆盖在文字上面的一层遮罩。利用background-clip: text配合背景位移动画,能实现“文字发光被擦拭”的感觉:
.sweep-loader { font-size: 2rem; font-weight: 700; color: #666; -webkit-background-clip: text; background-clip: text; background-image: linear-gradient(90deg, #666 0%, #1677ff 50%, #666 100%); background-size: 200% 100%; animation: sweep 1.5s linear infinite; } @keyframes sweep { 0% { background-position: 0% 0; } 100% { background-position: -200% 0; } }这段的原理比较容易绕晕,我拆开说:文字设置了透明背景、渐变填充和background-clip: text后,背景只在文字形状内部可见。接下来把background-size放大到 200%,再通过background-position让渐变左右平移,就形成了高光从左扫到右的效果。
background-position的百分比计算逻辑和普通定位不一样,它计算的是“图片的某个点对应容器某个点”,当background-size是 200% 时,background-position从 0% 走到 100% 意味着完整的位移。上面我让它走到-200%,是为了让移动方向和正向一致,这也是玄学成分最高的参数之一。遇到扫光方向反了时,不用改动画,先检查起始 background-position 和结束值方向是否互补,通常能解决。
2.4 为什么选 CSS3 而不是 GIF、APNG 或动画库
这里做一个不走套路的选型对比:GIF 是最常见的加载图格式,但缺陷很明显,8 位色深导致颜色有肉眼可见的色阶,半透明边缘处理差,放在深色背景上一圈白边框直接毁掉页面质感。APNG 解决了透明问题但体积大,文件动辄几百 KB。Lottie 效果上限高,但要引入 JSON 解析库,配置成本太大,小场景杀鸡用牛刀了。
CSS3 的方案在加载时序上也有天然优势:图片加载是页面资源请求的一部分,存在等待时间;CSS 动画没有额外网络请求,样式表已经在渲染时一并到达。需要修改颜色的时候,GIF 必须回到设计工具里导出新文件,CSS 动效改一个color或background-image就能生效。配合 CSS 变量,运行时换一套主题色甚至不需要改文件。
3. 一套可复现的文字加载动效:HTML、CSS 与 JS 收放状态的最小闭环
3.1 HTML 结构:给每个字一个可独立动画的包裹层
前面讲了动画原理,这一章直接给一套完整可复用的源码实现。为了兼容“JS 特效”这个关键词,我会把浏览器的加载提示、按钮的 loading 状态、请求过程中的占位提示都覆盖到。
先看 HTML 结构。动效要作用于单个字符,所以每个字符和点号都单独用span包裹:
<div class="loader" id="pageLoader" role="status" aria-label="加载中"> <span class="loader-char">加</span> <span class="loader-char">载</span> <span class="loader-char">中</span> <span class="loader-char loader-dot">.</span> <span class="loader-char loader-dot">.</span> <span class="loader-char loader-dot">.</span> </div>这里做了三件事。第一,每个字符依赖span才能独立位移和延迟,如果直接把整个文字串放在一个元素里,逐字动画就无从谈起。第二,role="status"给读屏软件明确的“状态变化”信号,这是很多前端会漏掉的细节。第三,三个.loader-dot点号复用同一套样式,后面可以让它们单独做省略号循环,这是“加载中”最常见的信息表达方式。
3.2 CSS 动画主体:弹跳加光晕,外加三个分隔点的独立节奏
完整的 CSS 分成三块:容器样式、字符动画、分隔点动画。我把它写成一个可直接粘贴的完整片段:
:root { --loader-duration: 1.4s; /* 单字符周期 */ --loader-distance: 0.4em; /* 弹跳高度 */ --loader-color: #1677ff; /* 主题色 */ } .loader { display: flex; gap: 0.05em; font-size: 1.5rem; color: var(--loader-color); } .loader-char { display: inline-block; animation: char-bounce var(--loader-duration) ease-in-out infinite; } .loader-dot { animation-name: dot-pulse; animation-duration: var(--loader-duration); animation-timing-function: ease-in-out; animation-iteration-count: infinite; } @keyframes char-bounce { 0%, 100% { transform: translateY(0); text-shadow: 0 0 rgba(22, 119, 255, 0); } 35% { transform: translateY(calc(-1 * var(--loader-distance))); text-shadow: 0 0.3em 0.6em rgba(22, 119, 255, 0.45); } 60% { transform: translateY(0); } } @keyframes dot-pulse { 0%, 20% { opacity: 0.2; } 50% { opacity: 1; transform: scale(1.2); } 80%, 100% { opacity: 0.2; transform: scale(1); } }char-bounce里用了两层技巧:位移用translateY由容器 flex 布局保证不产生换行;text-shadow在最高点展开,形成“弹跳时带光晕”的视觉。calc(-1 * var(--loader-distance))这行不加括号的话,整个表达式会失效,实际写代码时尤其要注意乘号两侧不能省略。
分隔点动画单独走dot-pulse,它和字符动画使用同一周期但不同缓动。这样三个点不会跟随文字做上下跳,而是原地明灭缩放。很多现成效果的“翻车点”就在这:把点也套进同一个动画名,出来的画面上三个点跟着文字一起飞,观感特别乱。
3.3 用 JS 控制加载态:出现、结束与隐藏后的清理
CSS 把动画跑起来了,但页面不能一直显示“加载中”。这段 JS 负责在需要时挂载容器,结束后移除,避免动画在不可见状态下持续消耗性能:
const loader = document.getElementById('pageLoader'); function showLoader(text = '加载中') { // 如果传入自定义文案,直接替换字符 loader.textContent = text; loader.style.display = 'flex'; requestAnimationFrame(() => loader.classList.add('is-active')); } function hideLoader() { loader.classList.remove('is-active'); // 用 setTimeout 等一个动画周期,避免闪没 setTimeout(() => { loader.style.display = 'none'; }, 300); } window.__showLoader = showLoader; window.__hideLoader = hideLoader;requestAnimationFrame的加特是血泪换来的:如果在 display 由 none 变为 flex 的同一个执行栈里直接加动画类,部分浏览器会把两个样式的变更合并,动画不会播放。下一帧再添加,能确保动画从头开始。隐藏时我留了 300ms 宽限期,刚好覆盖一个动画周期的一部分,让结束动作看起来平滑。
textContent直接替换字符串的做法省事,但注意替换之后原来的三个小数点没有了。真正要用省略号效果时,要么在 HTML 里保留点号标签,要么在 JS 里用模板字符串重新拼一段字符片段,两种方案都行。依赖点号动画的页面,我通常选择读 HTML 里的占位结构,只改前三个字。
4. 参数怎么调才不翻车:速度、延迟、颜色与文字内容改动的四个细节
4.1 参数速查表:从 0.8s 到 2s 的观感变化
CSS 动画的参数系统并不复杂,但乱调一通会得出很“土”的效果。下面这张表是我在几个项目里实测出来的推荐区间,可以直接做初始值,再基于手感微调:
| 参数项 | 推荐区间 | 过快的表现 | 过慢的表现 |
|---|---|---|---|
| 单字符动画时长 | 0.8s – 2s | 眼花,紧张 | 用户以为卡死 |
| 字符延迟间隔 | 0.05s – 0.2s | 排队感消失 | 文字被拆散 |
| 弹跳高度 | 0.2em – 0.5em | 夸张,不专业 | 几乎看不到 |
| 光晕模糊半径 | 0.3em – 0.6em | 文字发虚 | 失去存在感 |
| 无限循环次数 | infinite 或 3–5 次 | 耗电且干扰阅读 | 提前结束信息不完整 |
特别注意表格里的第二行“字符延迟间隔”。延迟间隔和单字符动画时长需要联动调整,常见做法是把间隔设为总时长的 10% 到 15%。比如动画时长 1.4s,那延迟就是在 0.14s 到 0.21s 之间取一个值,四五个字符排下来,整体波浪才协调。如果你只调时长不调延迟,延迟占比会变小,字符差不多同时起跳,波浪感直接消失。
4.2 文字内容改动的三个注意点:字符数、中英文混排、容器宽度
Loading 文字不会永远是“加载中”,用户可能传“请稍候”也可能传英文 “Loading”。改动时最容易出问题的,是字符数和换行。
第一个注意点,字符数变了,容器宽度也要跟着变。因为每个字符是 inline-block,宽度不够就直接换行,动效会从一排变成两排。好的防法是给.loader加white-space: nowrap,同时在容器上预留宽度安全位,或者直接使用flex布局,这是我前面的代码里已经写过的处理方式。
第二个注意点,中英文混排时0.4em这类相对单位是安全的,因为 em 是相对于当前元素字号计算的。中文一个字占一个 em,英文字符通常只有 0.5em 左右,弹跳高度看起来会不一样。要让英文动效观感统一,把“正在加载”和“Loading”分开两套样式,或者统一用固定像素值。
第三个注意点,text-shadow在动画关键帧里写了一次,后面如果你把文字颜色改成灰白色,光晕颜色仍是主题蓝,视觉会冲突。要么把光晕色也提取为变量,要么把光晕放到::before伪元素里,避免和文字颜色耦合。
4.3 场景扩展:按钮内嵌、弹窗复用、多主题切换
同一套动效,不只可以放在页面中央,也可以塞进按钮和弹窗。放进按钮时,需要控制动效的字号与按钮高度匹配:
.btn-loading { display: inline-flex; align-items: center; justify-content: center; min-height: 40px; } .btn-loading .loader { --loader-distance: 0.25em; font-size: 1rem; color: currentColor; }这里把--loader-distance覆写成 0.25em,是为了在较低的按钮高度里不至于让字符超出按钮上边界。颜色用currentColor自动继承按钮文字色,这样按钮是红色主题时加载动画也自动变红,不需要额外写主题变量。
如果项目里有多套主题,建议把颜色和时长全部收敛到:root的变量,运行时通过给容器加一个>@supports (-webkit-background-clip: text) { .sweep-loader { -webkit-background-clip: text; background-clip: text; } }
这个思路也适用于其他新特性。先保证基础可读性,再叠加视觉效果;对加载提示来说,文字看不看得清永远比动效炫不炫重要。
5.5 页面长时间挂着加载动画,CPU 和耗电飙升
现象:开发时看不出问题,上到低端安卓机后页面发烫,浏览器 debug 面板显示 GPU 进程占用高。
原因:无限循环加多个字符同时做动画,每个字符都会单独触发一次渲染。动画中如果还带很大的text-shadow模糊区域,模糊计算的代价不低,尤其在低端设备上会被放大。
解决:第一,动画结束后把display设为none;第二,同一时刻并发动画元素控制在 10 到 20 个以内,分页或者只保留当前可见部分;第三,把text-shadow换成不涉及模糊的简单光晕,或者减少光晕半径。真的需要大面积模糊效果时,用一张透明 PNG 光晕图做背景,渲染成本反而更低。另外,给系统偏好设置做响应是一个加分项:
@media (prefers-reduced-motion: reduce) { .loader *, .loader-char { animation: none !important; } }尊重用户的“减弱动态效果”系统设置,是资深工程师的标准习惯,也会直接过滤掉一批潜在的兼容性反馈。
6. 进阶用法:CSS 变量加状态机把 Loading 动效变成可复用组件
再往下走,我不会满足于“能跑就行”,而是想把这份源码包里的动效变成一个真正好维护的组件。两个关键手法:用 CSS 变量统管节奏,用 JS 状态机统管生命周期。
CSS 变量的作用不只是换颜色,更重要的是让延迟和动画时长联动。在字符动画里可以这样定义一个带计算延迟的规则:
.loader-char { animation-delay: calc(var(--loader-char-index, 0) * var(--loader-delay-step, 0.14s)); }使用方不需要知道每个字符的序号,只需要在模板渲染时给第i个字符写上style="--loader-char-index: i",延迟自动跟着字符位置走。这样整体节奏调起来只需要改一个--loader-delay-step,不需要翻页改十几个nth-child。把变量定义在.loader容器上,组件内所有子元素都能继承。
JS 状态机的思路是把 loader 的所有状态收拢到一个对象里:idle、loading、done、error。请求开始时进入loading,拿到响应进入done,失败时进入error并替换文案。隐藏时不能只改 display 就结束,而是一套完整的序列:先停止动画循环,再置灰容器,最后挂上aria-hidden。
class LoaderController { constructor(root) { this.root = root; this.status = 'idle'; } setStatus(status, message) { this.status = status; if (message) this.root.textContent = message; if (status === 'loading') { this.root.style.display = 'flex'; this.root.classList.add('is-active'); } else { this.root.classList.remove('is-active'); setTimeout(() => { this.root.style.display = 'none'; this.root.setAttribute('aria-hidden', 'true'); }, 300); } } }使用方式简单到不会干扰业务代码:controller.setStatus('loading', '请求中'),请求结束后controller.setStatus('done')。这里值得留意的一点是textContent会清掉所有子元素,所以如果你还想保留那三个点号,就把点号也改造成可以直接用appendChild生成的 DOM 节点,或者不在替换文案时动点号容器。
从那以后,我每次做加载动效都会强制走一遍这样流程:先确认animation-fill-mode,再测试background-clip的兼容,最后把 display 状态的切换放在动画收尾之后。这三个顺序就是这套源码里的隐藏精华,大部分线上问题都出在这几步上。说到调试,也建议你用 DevTools 的 rendering 面板把 “Paint flashing” 打开,看动画过程中哪些区域在持续重绘——只要绿色闪动面积大,就说明动画写得不干净,值得回头按第 5 章的思路优化一遍。希望帮到你。
本文还有配套的精品资源,点击获取