简介:纯CSS3实现的Loading文字加载动效是一份面向Web前端开发者的轻量级源码,无需JavaScript即可完成动态加载状态展示,适合用于页面加载初期提升等待体验,也适合对CSS动画感兴趣的初中级开发者作为学习范例。资源包共7个文件,含3个CSS样式文件、2个JS辅助脚本及HTML演示页面,压缩包仅7KB,结构简洁,便于直接嵌入或二次定制。已有281人学习下载,适合快速查阅动画实现思路。核心价值在于完整还原了利用CSS3 animation属性制作文字上下滚动效果的过程,涉及动画名称、时长、缓动曲线、延迟、方向及循环次数等关键子属性的综合运用;同时提供splitting文字拆分辅助脚本,便于理解和扩展更多文字动效,未来可在此基础上实现左右滑动、旋转或淡入淡出等效果,为网页交互增添吸引力。
1. 纯CSS3文字加载动效:不靠JS,几个span也能把“加载中”做活
一个后台登录页,原来那张转圈GIF在弱网下裂掉,整个页面看起来像“死”了。后来把“加载中”三个字换成逐字呼吸加轻微浮动的纯CSS3文字加载动效,体积从几十KB动图降到不足1KB样式,加载失败时文字本身也不会裂开。这套效果不依赖JS定时器,不需要图片或字体库,只要HTML里摆好文字,CSS3的关键帧自己就会一直跑。反直觉的地方在于:想做出“文字逐字错峰跳动”的感觉,不需要JS每隔几百毫秒改一次样式,给每个字符一个递增的animation-delay就够了。它适合SSR页面、静态站点、接口慢但首屏必须轻量的场景;前端、全栈,甚至只写模板的开发者都能直接拿来改。
2. 文字动画背后的CSS3模型:关键帧、错峰延时与合成层
2.1 动画三件套:@keyframes管“变成什么样”,animation管“怎么播”
CSS3动画由两部分组成。@keyframes定义关键时刻的样式快照,比如透明度0.3、位移4px;animation决定这个快照怎么播放:持续时间、缓动曲线、是否循环、要不要延迟。对文字加载动效来说,@keyframes通常只需要两个关键帧,0%和100%一个状态,50%另一个状态,剩下的中间帧交给浏览器插值。播放次数设成infinite,动画就会一直循环。
动画不需要用户触发,只要元素带着animation属性进入渲染树就开始跑。这对Loading场景非常关键——JS控制加载状态往往要等某个事件,CSS动画则是元素存在即运行。常见写法是animation: breath 1.2s ease-in-out infinite,这一行里的顺序是名称、时长、缓动、循环,具体顺序容易写错,到第5章会展开讲。
2.2 文字不是普通盒子:为什么transform在span上“失灵”
很多人第一次做文字动画时,把span直接translateY(4px),结果纹丝不动。原因是span默认是inline级元素,inline元素的transform属性不生效,浏览器会直接忽略。想让文字在行内逐字位移,必须先把spandisplay:inline-block。这一步不做,后面所有位移类动画全是空气。
转成inline-block后,还要注意行高问题。如果Loading文字和前后文字混排,inline-block的基线对齐会让文字轻微上下错位,一般给span显式设置line-height,或者让整个加载区块独立成行。另外,逐字浮动的幅度如果超过字号10%,视觉上会碎成一串上下乱窜的字,而不是“呼吸感”。这里有个细节:动画opacity时inline元素可以正常作用于透明度,所以如果只做淡入淡出,不改display也能跑,但一旦加位移,inline-block就是必选项。
2.3 逐字错峰的核心:delay数列与--i自定义属性
逐字动画的最基础模型是:给第n个字符追加第n级延迟。假设四个字“加载中”,每个字动画0.2秒,第二个字比第一个晚0.15秒,第三个再晚0.15秒,视觉上就形成一条波浪。
下面是最小可运行版本,HTML负责摆骨架,CSS负责动效。
<div class="loading-text" aria-label="加载中"> <span style="--i:0">加</span> <span style="--i:1">载</span> <span style="--i:2">中</span> <span style="--i:3">…</span> </div>.loading-text span { display: inline-block; font-size: 24px; color: #333; animation: breath 1.2s ease-in-out infinite; animation-delay: calc(var(--i, 0) * 0.15s); } /* 关键帧:让文字在“半透明+下沉”和“实色+原位”之间循环 */ @keyframes breath { 0%, 100% { opacity: 0.35; transform: translateY(4px); } 50% { opacity: 1; transform: translateY(0); } }逻辑说明:每一帧里opacity和translateY同步变化,形成一个“按下去又弹回来”的节拍。4个字符的delay分别是0s、0.15s、0.3s、0.45s,最终呈现从“加”开始依次下压、依次回弹的波浪。
参数说明:delay用了calc(var(--i) * 0.15s),把每个span的序号通过CSS自定义属性传入,这样增删字符不用改CSS文件,只改HTML里的数量。想要节奏更快,把0.15s改成0.1s;字体更大,建议把duration从1.2s加到1.4s。这里不建议直接把delay写死在每个span上,维护起来非常痛苦,后面第4章会给出延时与周期的计算关系。
2.4 属性选型:为什么只推荐opacity和transform组合
浏览器渲染一帧动画时,不是所有属性都同样便宜。改变opacity和transform走的是合成器线程,通常不触发Layout和Paint重绘;改变color、letter-spacing、font-weight、width等属性,会让浏览器重新计算布局或重绘像素。对一段循环播放的Loading动效来说,重绘面积越大,低端安卓机掉帧越明显。
所以做纯CSS3文字加载动效时,我一般只让opacity和transform进@keyframes。闪烁用opacity替代font-weight变化,浮动用translateY替代margin-top,淡入用opacity替代visibility。如果确实想加一点模糊光泽感,用filter: blur要非常谨慎,后面第5章会讲到它在低端机上的代价。will-change属性在这个场景里基本不需要加,动画元素只有几个字,浏览器自动优化的效果已经够好,滥用will-change反而会多占显存。
| 目标效果 | 推荐属性 | 避免属性 |
|---|---|---|
| 呼吸淡入 | opacity | visibility |
| 上下浮动 | transform: translateY | margin-top / top |
| 文字粗细闪烁 | opacity | font-weight |
| 字距变化 | 尽量不做 | letter-spacing |
3. 三种可直接抄的文字加载方案:逐字呼吸、扫光与三点循环
3.1 逐字呼吸+轻微浮动:最通用的“加载中”动效
这个方案就是第2章最小版本的完整形态,适合中文词语,也适合“LOADING”。
<div class="word-loading" role="status" aria-label="加载中"> <span style="--i:0">加</span> <span style="--i:1">载</span> <span style="--i:2">中</span> <span style="--i:3">.</span> <span style="--i:4">.</span> <span style="--i:5">.</span> </div>.word-loading { display: inline-flex; gap: 2px; font-size: 28px; color: #111; } .word-loading span { display: inline-block; animation: wordBreath 1.3s cubic-bezier(0.4, 0, 0.2, 1) infinite; animation-delay: calc(var(--i, 0) * 0.12s); will-change: opacity, transform; } @keyframes wordBreath { 0%, 100% { opacity: 0.3; transform: translateY(5px) scale(0.98); } 50% { opacity: 1; transform: translateY(0) scale(1); } }逻辑说明:内联flex把span排成一行,gap控制字间距,避免动画时字符碰在一起。每个span的delay由--i递增,形成从左到右依次下压的波浪。translateY和scale同步作用,比纯位移多一层“靠近又远离”的纵深感,但仍属于合成属性,没有引入重绘。
参数说明:字号28px时位移5px,约为字号的18%,适中偏克制。如果字号缩到16px,位移要压到3px以内,否则小字上下乱跳会显得廉价。duration这里选了1.3s,节奏偏舒缓,适合正式系统;偏活泼的场景可以压到0.9s。另外给的aria-label和role="status"是给读屏用户看的,纯CSS动画本身不传递语义,loader需要靠这类属性补全。
3.2 扫光文字:用background-clip:text做渐变穿过效果
扫光文字是另一种常见玩法:文字本身保持一个基础色,一道高光反复从文字表面穿过。实现不再依赖透明度和位移,而是让背景渐变在文字内移动。
<div class="shine-text">加载中 LOADING</div>.shine-text { font-size: 36px; font-weight: 700; background-image: linear-gradient(90deg, #888 0%, #ddd 40%, #fff 50%, #ddd 60%, #888 100%); background-size: 200% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; animation: textShine 2s linear infinite; } @keyframes textShine { from { background-position: 200% center; } to { background-position: -200% center; } }逻辑说明:background-image被裁剪到文字轮廓内,color设为transparent,文字的可见内容就是渐变本身。background-size设为200%,让渐变宽度超出元素;背景位置从200%移到-200%,视觉上高光从右往左穿过整行文字。
参数说明:渐变中间白色高光段越窄,扫光越像“一道光”;越宽越像整体变色。2s周期一般够用,文字长就加到2.5s,短就缩到1.6s。这段代码有个致命前提:浏览器必须支持background-clip:text。现代桌面浏览器基本都支持,但部分老安卓WebView需要-webkit-前缀,且一旦不支持,color:transparent会让文字整个消失。第5.3节会给出完整的防翻车回退写法,这里先记住一个原则——高级效果必须有基础态兜底。
3.3 省略号三点与闪烁光标:用steps()实现的轻量打字机
有人想要打字机效果。纯CSS做不了真正的逐字打出,因为字符出现需要动态改变内容或宽度,内容变化是CSS月媚能做的。但打字机最有辨识度的两个元素——光标闪烁和末尾省略号循环——可以完整用CSS实现。
省略号循环的实质是三个点轮流出现。这里用负delay来错峰,因为循环动画从中间相位插入,能让三个点在每一圈保持固定间隔。
<div class="dot-loading"> 正在加载<span class="dots"><i>.</i><i>.</i><i>.</i></span> </div>.dot-loading { font-size: 18px; color: #333; } .dots i { font-style: normal; opacity: 0; animation: dotCycle 1.2s infinite; } .dots i:nth-child(2) { animation-delay: -0.4s; } .dots i:nth-child(3) { animation-delay: -0.8s; } @keyframes dotCycle { 0% { opacity: 0; } 15% { opacity: 1; } 60% { opacity: 1; } 100% { opacity: 0; } }逻辑说明:每个点只在动画周期的15%到60%之间显示,其余时间隐藏。第二个点延迟-0.4s、第三个延迟-0.8s,等效于把相位均匀错开三分之一,任何时候都只有一个点亮着,看起来像打字机在持续输入。
参数说明:1.2s是一般速度,偏快可以0.9s。注意这里delay用了负值,这是循环错峰和一次性错峰的分界线:正整数delay只影响第一次播放,循环开始后所有动画相位会恢复为同起点;负值则让动画理解为一出生就走到了周期中间,循环里也能保持错位。光标闪烁可以再加一个元素复用一个blink关键帧:
.cursor { display: inline-block; width: 2px; height: 1em; background: #333; animation: blink 1s steps(2, start) infinite; } @keyframes blink { to { visibility: hidden; } }steps(2, start)让光标在0到0.5s可见、0.5s到1s隐藏,不是渐隐渐显而是干脆利落的开关状态,更像真实光标。边界说清楚:这套方案只是“打字机氛围”,真正的逐字上屏还是需要JS控制定时器或前端框架来追加字符。
4. 让动效不廉价的关键参数:时长、错峰与曲线怎么配
4.1 时长与字号的关系:大字号要慢半拍
视觉节奏有一个朴素规律:元素越大,完成一次运动所需时间越长。24px以下的小字用0.9s一周期正好,28px到36px的大标题要1.3s到2s,否则会让用户觉得页面上有什么东西在“急闪”。扫光效果尤其明显:36px字号用1s扫一遍,光带快到像刷新故障;拉到2s左右,才有“光从表面滑过去”的质感。
时长翻倍的同时,透明度低谷也要跟着调整。小字号动画可以把opacity压到0.2,因为可读性压力小;大字号是视觉主体,透明度低于0.3就会产生“文字消失”的错觉。Loading动效的底线是任何时刻整体语义可读,这个我在4.4再展开。
4.2 错峰间隔的数学:delay不是随手填的
逐字动画的错峰间隔,决定整串文字的波浪感。间隔越大,波浪越明显,但也容易让用户觉得前一个字已经“死”了后一个字才动。经验值是间隔控制在单个动画周期的8%到15%。1.2s周期配0.1s到0.15s间隔,观感最顺。
周期和间隔之间存在一个容易忽视的关系:如果所有字都用正delay,最后一字的延迟由delay加上动画duration决定。比如duration=1.2s、delay=0.15s、六个字,最后一字在0.75s后才开始播,总周期实际是1.2+0.75=1.95s。这时循环错峰并没有变,只是前0.75s只有部分字在动。想要前几个字从一开始就进入“错峰状态”,有两条路:一是给每个span设置animation-fill-mode: both,让延迟期间就应用关键帧的第一帧;二是直接用负delay。前者代码语义更清楚,适合“加载中”这种一次性出场文字;后者适合三点循环这类需要立刻满负荷跑的动效。
4.3 曲线调“呼吸感”:比ease-in-out更耐看的选择
默认的ease和ease-in-out做科技感UI时会显得机械,因为它们的加速减速完全对称,像节拍器。做“呼吸感”我常用cubic-bezier(0.4, 0, 0.2, 1),即Material Design标准曲线,先快后慢,文字“呼”地落下来,再缓缓“吸”回去,视觉重心比ease-in-out靠前。
如果要更明显的弹性,可以用cubic-bezier(0.34, 1.56, 0.64, 1),文字落到原位时会轻微过冲再回弹。但Loading场景不建议,因为无限循环下过冲会反复闪现,看三秒以上会疲劳。扫光和三点循环这类匀速动作用linear即可,它们本来就该均匀、不要加减速。
4.4 幅度与透明度边界:别把文字弄得不可读
透明度低谷建议不低于0.3,位移幅度不超过字号的15%。这两个值同时越界,比如光标动画中透明度压到0.1再下沉8px,整段文字在每一轮的某个瞬间几乎消失,用户会以为加载出错。我自己检查Loading时有一个笨办法:在浏览器动画面板里暂停动画,分别看25%、50%、75%三个中间帧,如果任何一帧文字难以辨认,就把幅度调小。
另外,逐字动画的波浪只在文字自身内部进行,不要让整行文字整体位移。整行移动会让页面其他内容跟着抖动,也容易让用户误以为是页面布局在“晃动”。如果确实想整行呼吸,就把动画加在容器上而不是每个span上,两者效果完全不同。
4.5 参数速查表:三套方案的推荐配置
| 方案 | duration | delay间隔 | 曲线 | 备注 |
|---|---|---|---|---|
| 逐字呼吸 | 1.2s–1.4s | 0.1s–0.15s | cubic-bezier(0.4, 0, 0.2, 1) | 字号越大,时长越大 |
| 扫光 | 2s–2.5s | 不使用 | linear | 文字过长就增加时长 |
| 三点循环 | 0.9s–1.2s | -0.4s / -0.8s | linear | 一定要负delay |
| 光标闪烁 | 1s | 不使用 | steps(2, start) | 用steps模拟开关 |
5. 纯CSS3文字加载动效避坑手册:5个翻车现场与排查路径
5.1 所有字先齐跳一回合,第二圈才开始错峰
现象:第一遍播放所有span同时起跳,从第二遍开始才出现波浪错峰。原因:把animation-delay写成了正值,正delay只推迟第一次播放,循环开始后各元素从同一起点重新计数;更隐蔽的是animation简写里时间参数顺序错了,比如animation: breath 1.2s 0.2s infinite,这里第二个时间值被解析成了delay,但忘记写iteration-count为infinite,或者把两个时间值位置写反。解决:检查简写,标准顺序是animation: name duration timing-function delay iteration-count direction fill-mode;如果已经写对但还是要第一遍就错峰,给每个span同时加animation-fill-mode: both,让delay期间先应用第一帧样式,也可以把delay改成负值,让动画一开始就处于错峰相位。
5.2 动画过程中文字左右乱晃、行内排版崩
现象:Loading文字在动画时除了上下浮动,还出现左右错位、字符间忽宽忽窄,或者把周围文字顶得上下跳。原因:span没有转成inline-block,位移作用无效;另一种原因是@keyframes里面动了letter-spacing或font-weight,每个字符宽度变化,行内排列自然被推挤。解决:所有做位移的span一律display:inline-block;@keyframes里只保留opacity、transform,禁止改font-weight、letter-spacing、width这类会触发布局的属性。如果必须让字距变化,把它放在容器上用transition做一次性效果,不要放进无限循环动画。
5.3 扫光文字变成白条或整行消失
现象:代码在开发机上正常,在客户的老安卓手机和部分老旧Windows浏览器上,文字完全看不见,只剩一块空白。原因:background-clip:text在非WebKit内核的支持不完整,color:transparent一旦生效,而裁剪没有生效,文字本身就没有填充色。解决:加@supports回退,把基础色写成普通color,只有检测到支持background-clip:text时才透明化并应用渐变:
.shine-text { color: #666; /* 兜底:不支持裁剪时依然是可读的文字 */ } @supports (background-clip: text) or (-webkit-background-clip: text) { .shine-text { background-image: linear-gradient(90deg, #888 0%, #ddd 50%, #888 100%); background-size: 200% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; animation: textShine 2s linear infinite; } }逻辑说明:@supports里先写标准属性再写-webkit-前缀,没有匹配到就保持灰字。即使浏览器完全不认这个特性,用户看到的也只是一行静止可读的文字,不会变成空白。
5.4 低端机动效卡到掉帧,CPU占用持续过高
现象:含filter:blur、letter-spacing或大面积背景渐变动画的Loading,在低端安卓机型上肉眼可见卡顿,Debug面板里Paint和Layout记录密集。原因:filter:blur每一帧都要对文字周围做高斯模糊,手机GPU压力大;letter-spacing变化触发部分重排;渐变背景和background-position动画每帧重绘面积较大。解决:优先用opacity和transform组合,把模糊效果改成预置的半透明层加opacity动画;扫光的background-position动画如果同样卡,把渐变范围缩窄、background-size从200%改成150%,或者干脆用伪元素覆盖实现光带,减少每帧重绘面积。
5.5 动画结束后文字永久消失
现象:动画只播放一遍,结束时文字消失,页面上看不到“加载中”三个字。原因:@keyframes的100%帧写了opacity:0,同时animation设置了animation-fill-mode: forwards,让最终帧被保留;而Loading循环动画根本不需要fill-mode。解决:循环动画直接删掉fill-mode,或者明确写成infinite;如果预期加载完成后文字要淡出,不要依赖这个动画的最后一帧,把淡出逻辑放到容器上,用独立transition控制,避免和循环动画的相位纠缠。
6. 上线前最后一步:用DevTools验证渲染层,给动效一个“降级开关”
6.1 3分钟验证:录制Performance,画动画中间帧检查可读性
写完动效后,我常用两个步骤验证。打开DevTools的Performance面板,录制3到4秒动画过程,观察帧时间线和Layout记录。如果录制结果里大量出现紫色Layout或大面积Paint,说明动画用了会触发重排的属性,回头检查@keyframes里是否混入font-weight、letter-spacing等属性。理想状态是录制结果主要是Compositing,偶有零星的Paint,这是合成器属性该有的样子。
第二步是打开Rendering面板,勾选Paint flashing,页面里不断闪烁绿色代表发生重绘。文字Loading动效的范围很小,闪烁区域应该只集中在文字附近;如果整行甚至整个页面都在闪,说明动画区域过大或触发了兄弟元素的重绘。最后停在动画中间帧检查可读性,我会在动画面板里把播放头拖到约40%的位置,确认这段时间内即使某些字符透明,整体词仍能辨认。
6.2 尊重系统设置:prefers-reduced-motion降级为静态文字
很多项目忘了做动效降级,导致系统开启“减弱动态效果”的用户依然面对无限循环的闪烁。CSS动画在这种场景下应该退化为静态文本。
@media (prefers-reduced-motion: reduce) { .word-loading span, .dots i, .cursor, .shine-text { animation: none !important; opacity: 1; transform: none; } }逻辑说明:媒体查询命中后,所有loading动画停止播放。三点循环的每个点通过opacity:1恢复完整显示,扫光文字因为本身没有透明度动画,animation:none后保持渐变静态填充,文字依旧可读。这样既保留了Loading的功能性,又不会让对运动敏感的用户被来回扫动打扰。
我现在的习惯是每次交付前花三分钟走这遍验证:先看Performance里的Layout条是否干净,再暂停动画看中间帧是否可读,最后切到系统“减少动态效果”模式确认静态兜底正常。动效再小也是体验的一部分,栽过扫光白屏和循环错峰的跟头之后,这两个习惯基本没让动画问题再回流过。希望帮到你。
本文还有配套的精品资源,点击获取