资讯动态

纯CSS3文字加载动效:从animation到性能优化全指南

发布时间:2026/10/6 3:40:16 来源:尧图企业网站定制
简介纯CSS3 Loading文字加载动效是一套不依赖 JavaScript 的轻量加载动画源码面向前端开发者与网页设计者用于页面加载初期展示文字滚动、拆分、回弹等动态反馈兼顾美观与低资源消耗。压缩包内共 7 个文件包含 3 个 CSS、2 个 JS、1 个 HTM 与 1 个 HTMLCSS 负责拆分文字并驱动动画节奏JS 辅助完成文字字符切分HTML 示例页可直接运行查看效果整体仅 7KB。目前已有 281 人学习下载适合前端入门者对照源码理解 animation 语法也适合直接改造成站内 Loading 组件。资源完整提供了可运行示例与配套样式覆盖 animation-name、duration、timing-function、delay、direction、iteration-count、fill-mode、play-state 等核心属性的组合用法通过调整 duration 可控制滚动快慢delay 可实现字符依次启动direction 可控制上下往复并附 splitting 工具脚本便于扩展左右滑动、旋转、淡入淡出等多种文字动效提升加载过程的交互表现与页面设计感。1. 纯CSS3 Loading文字加载动效不靠图片和JS也能把等待变成体验纯CSS3 Loading文字加载动效看起来只是一排字符在动但它解决的问题很具体页面或接口在等待时用户需要一个明确、轻量且不打断操作节奏的反馈。我第一次做是在一个后台系统的首屏不想为几个字符引一张 GIF 动图更不想为这点小事挂一个 JS 定时器最后靠animation和keyframes做了一组逐字跳动的 “LOADING”首屏轻了很多后来团队直接沿用这套样式。这里不依赖任何框架和构建工具一段 HTML 配一段 CSS 就能跑。踩坑的地方不在写不写得出来而在动画生命周期、节奏控制和兼容降级。按常用落地路径我会把三类效果、关键参数和五个高频问题一次讲清楚。当你把加载从一张静态图变成一组会起伏的文字等待体验会上一个台阶而成本只是几 KB 的 CSS。2. CSS3动画的最小骨架animation 与 keyframes 是怎么让文字动起来的2.1 一个最简文字动画从 keyframes 到 animation 简写很多人接触 CSS 动画的第一反应是找现成库但一个文字加载动画连库都算不上只需要理解keyframes定义“从哪到哪”animation声明“怎么播”。先看一个最简的例子一个字符上下跳动。p classsimple-load正在加载span classthe-char。/span/pkeyframes bounce-char { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-6px); } } .the-char { display: inline-block; /* 重点让 transform 能作用于字符 */ animation: bounce-char 0.8s ease-in-out infinite; }这里有个新手必踩的点display: inline-block必须先写。transform对纯内联元素不生效浏览器会直接忽略位移字符纹丝不动。keyframes里的0%, 100%定义了起点与终点50%定义中间帧animation简写则把帧名、时长、速度曲线、循环次数一次性声明。animation简写的顺序问题值得单独说一段animation: bounce-char 0.8s ease-in-out infinite里第一个时间值是duration第二个时间值才是delay。比如animation: bounce-char 1s 2s infinite是“播 1 秒、延迟 2 秒、无限循环”。如果两个时间值只写一个浏览器默认它就是duration。这个顺序我至少见过三次被搞颠倒结果动画要么不播要么干等几秒才动。下面这几个子属性在文字加载场景里最常调子属性示例值在 loading 文字动效里的作用animation-namebounce-char指定要播放的关键帧组animation-duration0.8s单次循环时长决定节奏快慢animation-timing-functionease-in-out速度曲线决定起落是否生硬animation-delay120ms逐字错开的间隔波浪感的核心animation-iteration-countinfinite是否循环加载动画一般无限循环animation-directionalternate是否需要往返播放animation-fill-modeboth动画播放前/后是否保留关键帧状态animation-play-statepaused调试时暂停动画或者页面隐藏时停止日常调参基本只动四个duration、delay、timing-function、fill-mode。fill-mode的坑放到第 5 章专门讲它是“动画结束跳变”的头号原因。2.2 控制节奏的关键timing-function、steps() 与 delaytiming-function决定一个周期内速度怎么变化。ease先快后慢linear匀速ease-in-out两头慢中间快。文字加载效果里ease-in-out用得最多因为字符升起和落下都很柔和如果做“打字机光标”那种开关式闪烁linear更接近真实屏幕刷新。steps()则是不做补间、阶梯式跳变适合省略号“点点点”这类离散状态。举一个光标闪烁的写法.dot-blink { animation: dot-blink 1.2s steps(2, start) infinite; } keyframes dot-blink { 0% { opacity: 0; } 100% { opacity: 1; } }steps(2, start)让透明度在两个关键帧之间分段跳换而不是渐变过渡视觉上就是“亮-灭-亮-灭”的开关感。如果你想要一明一灭交替可以改成三个关键帧把50%设为opacity: 0.3再配合ease-in-out又会变成呼吸灯效果。delay在逐字动效里本质是“排队队列”第一个字延迟 0ms第二个字延迟 120ms第三个字延迟 240ms视觉上就形成扫描或波浪。延迟步长不能超过单次动画时长太多否则前面字符已经完成一个周期后面的还没开始整个动效会变成一段静止、一段运动。我的经验是delay-step duration × 0.8 ÷ 字符数以 7 个字符、1.2s 时长为例步长取 120ms 左右比较稳。2.3 为什么只改 transform 和 opacity选属性就是选性能浏览器渲染一张页面要经历样式计算、布局、绘制、合成。改动top/left、margin、width这类属性每一帧都要重新走布局和绘制十几个字符同时跳动主线程就可能撑不住。而transform和opacity不会触发布局也不会触发绘制直接交给合成器处理动画由 GPU 插帧主线程负担极小。动画属性渲染管线影响loading 场景建议transform: translateY合成首选opacity合成首选color绘制少量使用top / left布局 绘制不建议用于动效width / height布局 绘制不建议用于动效所以后面所有代码里的位移都用translateY明暗变化都用opacity。把动画属性固定在这两个里是文字加载动效不掉帧的最基本前提。如果你发现动画一卡一卡先别怀疑电脑配置先看是不是有人把margin-top写进了keyframes。3. 三种可直接复制的Loading文字加载动效代码、参数与替换思路3.1 逐字跳动 “LOADING”最常见的首屏加载文字逐字跳动是出现率最高的文字加载效果适合英文短单词和页面标题场景。完整结构如下。div classbounce-loading rolestatus spanL/spanspanO/spanspanA/spanspanD/spanspanI/spanspanN/spanspanG/span /div.bounce-loading { display: flex; font-size: 24px; font-weight: 700; letter-spacing: 4px; color: #2b6ef0; } .bounce-loading span { display: inline-block; animation: bounce-text 1.2s ease-in-out infinite both; } .bounce-loading span:nth-child(1) { animation-delay: 0ms; } .bounce-loading span:nth-child(2) { animation-delay: 120ms; } .bounce-loading span:nth-child(3) { animation-delay: 240ms; } .bounce-loading span:nth-child(4) { animation-delay: 360ms; } .bounce-loading span:nth-child(5) { animation-delay: 480ms; } .bounce-loading span:nth-child(6) { animation-delay: 600ms; } .bounce-loading span:nth-child(7) { animation-delay: 720ms; } keyframes bounce-text { 0%, 100% { transform: translateY(0); } 30% { transform: translateY(-10px); } 60% { transform: translateY(-4px); } }逻辑说明每个span是一个独立的动画元素。动画时长 1.2s 表示一个字符完成“起跳-回落”所需时间延迟从 0 到 720ms 把 7 个字符均匀铺满整个周期形成依次抛起的队列infinite保证循环both保证动画在任意时刻都使用关键帧状态不会在循环间隙跳回原位。参数说明想让跳动更明显把30%的-10px改成-16px想让节奏更急促缩短duration并同步缩短延迟步长字符数变化时需要按公式重算 delay否则整组运动会失去连续性。动态渲染场景里手写nth-child不现实更推荐用 CSS 自定义属性.bounce-loading span { animation: bounce-text 1.2s ease-in-out infinite both; animation-delay: calc(var(--i, 0) * 120ms); }span style--i: 0L/span span style--i: 1O/span span style--i: 2A/span这样后端或者前端渲染时只需给每个字符输出--i的值延迟自动跟着走。注意最终animation-delay是单独属性不会被写在前面简写里的同名值覆盖但如果你把animation简写放在animation-delay之后后者会被简写重置。写的时候把单独属性和简写分开顺序保持一致能少踩很多坑。3.2 波浪起伏的“正在加载”让文字像水波一样扫过波浪效果和逐字跳动的差别在于每个字符的位移不是单峰往返而是“上浮-下坠-回弹”的复杂曲线视觉上像一排文字被一阵风扫过。结构同样是把每个字符包在独立span里。p classwave-loading span正/spanspan在/spanspan加/spanspan载/spanspan中/span /p.wave-loading span { display: inline-block; font-size: 20px; animation: wave-text 1.6s ease-in-out infinite both; } .wave-loading span:nth-child(1) { animation-delay: 0s; } .wave-loading span:nth-child(2) { animation-delay: 160ms; } .wave-loading span:nth-child(3) { animation-delay: 320ms; } .wave-loading span:nth-child(4) { animation-delay: 480ms; } .wave-loading span:nth-child(5) { animation-delay: 640ms; } keyframes wave-text { 0%, 100% { transform: translateY(0); } 20% { transform: translateY(-8px); } 40% { transform: translateY(4px); } 60% { transform: translateY(-4px); } 80% { transform: translateY(2px); } }逻辑说明wave-text里 20% 先上浮到最高点40% 下坠到水下60% 再微浮80% 回到接近原位最后收在 100% 的起点整条曲线有“起伏-回弹”的过程比单向跳动更接近波浪。五个字符延迟各差 160ms波峰依次传递。参数说明波浪的时长通常比逐字跳动更长1.4s 到 1.8s 之间否则波峰还没形成下一个字已经压过来。延迟步长也要同步变大150ms 到 200ms 是常用区间。如果字符之间有字体间距不匀的感觉给父级加letter-spacing而不是给每个span加margin否则延迟队列会被间距干扰。中文场景下建议整组使用不要只让两个字符动幅度和错开时间都会显得突兀。3.3 省略号“点点点”与渐变流动文字两个短小但高频的效果“加载中…”是业务系统里最朴素的反馈做成纯 CSS 后不需要任何图片资源。先用span做点span classdots-loading span classtxt加载中/span span classdot/spanspan classdot/spanspan classdot/span /span.dots-loading { display: inline-flex; align-items: flex-end; } .dots-loading .dot { width: 4px; height: 4px; margin-left: 2px; border-radius: 50%; background: currentColor; animation: dot-blink 1.2s ease-in-out infinite; } .dots-loading .dot:nth-child(2) { animation-delay: 150ms; } .dots-loading .dot:nth-child(3) { animation-delay: 300ms; } keyframes dot-blink { 0%, 100% { opacity: 0.2; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-2px); } }逻辑说明三个点透明度从 0.2 到 1配合一点上浮形成“逐个亮起”的等待感。150ms 和 300ms 的延迟让三个点错开避免整体明灭。这里有个隐藏坑.dot:nth-child(2)匹配的是父元素下第二个子元素如果前面还有“加载中”文本节点计数不受影响因为nth-child只数元素节点。但如果你前面又加了一个.txt的span那么第一个点的选择器就变成了.dot:nth-child(2)后一个点变成.dot:nth-child(3)列表里的前两条延迟规则会错位。稳妥做法是给每个点单独下标或者把文本单独包一层后用点自身维护一个类名。另一个常见效果是“渐变流动”利用背景裁剪到文字让光泽扫过字符比单纯位移更精致.gradient-loading { font-size: 28px; font-weight: 700; color: rgba(0, 0, 0, 0.85); background: linear-gradient(90deg, #999 25%, #222 50%, #999 75%); background-size: 200% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; animation: shine 1.5s linear infinite; } keyframes shine { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }逻辑说明文字本体透明背景渐变被裁剪进字形里背景位移动起来就像一道高光在文字表面扫过。和透明度无关所以不会出现整组文字变暗的问题。参数说明background-size: 200% 100%让渐变宽度是容器两倍位置移动才有空间感animation时长 1.5s 扫一个来回太快会晃眼太慢会让人感觉加载停滞。特别注意不支持background-clip: text的浏览器里文字会变成完全透明必须用supports或先写一个实色color作为降级。下面的兼容段落会细说。效果核心属性单次时长延迟步长循环逐字跳动transform1.0 - 1.4s100 - 160msinfinite波浪起伏transform1.4 - 1.8s150 - 200msinfinite点点点opacity1.0 - 1.4s150 - 250msinfinite渐变流动background-position1.2 - 2.0s不需要infinite4. 掉帧、兼容性与无障碍让动效在各种环境里都站得住4.1 为什么你会掉帧先看渲染管线再选动画属性前面说过transform和opacity走合成器这是文字加载动画不掉帧的前提。但“前提”不等于“只要用了就万事大吉”。当一个页面里同时存在十几个infinite动画每个动画元素都会被提升为独立合成层CPU 和 GPU 的内存占用随之上涨。移动端低端机上可能出现整体滚动卡顿而不是动画本身卡。我一般这样控制loading 文字动效的字符数控制在 3 到 12 个之间超过 12 个先考虑缩短时长或减少位移幅度而不是继续堆元素。另一个问题是will-change被滥用。will-change: transform确实可以提前告诉浏览器做合成优化但它也会让元素长期占据独立图层。对只有几秒生命周期的 loading 动画来说收益远小于内存开销我不建议在文字动画上加它。一段反面写法可以直接说明问题/* 不建议margin-left 每帧都触发布局和绘制 */ .loading span { animation: move-left 1s linear infinite; } keyframes move-left { 0% { margin-left: 0; } 100% { margin-left: 20px; } } /* 推荐transform 不触发布局重算 */ .loading span { animation: move-left 1s linear infinite; } keyframes move-left { 0% { transform: translateX(0); } 100% { transform: translateX(20px); } }逻辑说明两条规则视觉位移一样但第一条让浏览器每帧重排整行文本第二条只移动合成层。加载动画本身就是短时高频运行选错属性会让整页交互掉帧。你可以在 DevTools 的 Performance 面板录制几秒看 Layout 和 Paint 任务是否长时间占满主线程。4.2 兼容性老浏览器、前缀与 supports 降级现代浏览器对animation和transform基本没有兼容性问题但老项目里仍可能遇到 iOS Safari 8或者企业内网里的 IE 兼容模式。前者需要手动补-webkit-前缀后者可能在动画播放到一半时出现布局抖动。我的处理方式是动画属于增强体验不是核心功能。让静态文字永远可见动效只是在支持的情况下额外提供。用supports做能力检测.bounce-loading span { /* 降级无动画时是普通英文字母 */ } supports (animation: bounce-text 1.2s infinite) { .bounce-loading span { animation: bounce-text 1.2s ease-in-out infinite both; } }逻辑说明不支持animation的浏览器会忽略supports块里的规则此时字母仍然完整显示只是不会跳动。支持动画的浏览器正常播放。这种做法比写一堆 hack 更干净。老版 Safari 如果连transform都不认那就需要把位移相关属性写成带-webkit-transform的版本或者干脆把位移幅度调小避免字符溢出行框。环境animationtransform关键注意现代浏览器支持支持直接用iOS Safari 8支持需要前缀手动补-webkit-IE10 / IE11支持支持不要在动画里改 flex 相关属性IE9 及以下不支持部分支持显示静态文字即可4.3 prefers-reduced-motion 与读屏动效不能掩盖信息很多操作系统提供“减少动态效果”选项用来缓解视觉疲劳。网页端对应prefers-reduced-motion媒体查询。不做这层适配你的 loading 动效在那些用户眼里就是一片乱跳的字符。media (prefers-reduced-motion: reduce) { .bounce-loading span, .wave-loading span, .dots-loading .dot { animation: none; } }逻辑说明用户开启减少动效后动画被关闭只保留静态文字。这也符合加载态的本意快速告知用户“正在等待”而不是表演特效。读屏方面给整个加载容器加rolestatus读屏软件会在状态变化时播报内容。不要用text-indent: -9999px把文字移出屏幕那会让辅助技术也读不到直接用上文的处理文字始终在文档流里。4.4 克制使用300ms 内的加载不要做动画这条是最容易被忽略的不是所有等待都适合放 loading 动画。接口在 300ms 内返回时loading 容器刚出现就被移除用户看到的是一闪而过的图案而不是“正在加载”的反馈反而增加认知噪音。常见做法是给 loading 容器加一个延迟出现机制比如用 CSS 动画本身配合延迟页面请求开始时不立即挂is-loading类等 300ms 后再挂。如果请求已经完成类就不会被挂上。文字加载动画只应对“确实需要等待”的场景这句话我写了五年前端还在反复提醒自己。5. 常见问题排查动画结束跳变、逐字延迟被覆盖等五类坑5.1 动画结束后文字跳回原样现象动画播完第一轮后整组文字瞬间回到起始状态或者出现明显的一次“抖一下”。原因animation-fill-mode默认值是none动画播放之外的时间不保留任何关键帧状态。对infinite动画来说循环间隙通常不明显但如果配合了delay在等待期间文字会以原始状态展示与动画起点不一致就会出现跳变。另一个常见原因是简写顺序写错animation: bounce-text 1s 2s both里两个时间值一前一后第二个才是延迟写反后文字要先停两秒再跳。解决给简写末尾补上both写成animation: bounce-text 1.2s ease-in-out infinite both。这句里没有显式delay时both会让动画在延迟期间也使用第一帧状态循环之间平滑衔接。5.2 逐字动画只有第一个字在动后面纹丝不动现象一排span只有第一个字反复跳动其余字符像焊死在页面上。原因大部分时候是选择器写窄了只选中了第一个子元素比如误用.load span:first-child后面没有给其他span配延迟。另一种情况是给整组span写了统一切换比如.load span { animation-delay: 0s; }所有字符同一帧运动视觉上看起来只有“一坨”在动。解决使用nth-child(n)逐个定义延迟或者用--i变量内联在字符上。排查时先打开 DevTools 的 Elements 面板逐个检查每个span的animation-delay和animation-name是否真的生效往往是某个高优先级规则把延迟重置了。规则优先级冲突是这类问题的常客尽量把animation-delay的单独声明放在animation简写之后避免被简写覆盖。5.3 delay 与 duration 比例失衡排队的等待黑洞现象动画播起来不连贯前半段所有字符静止后半段才连续动起来或者动一下就长时间不动。原因延迟步长相对于动画时长过大。比如单字动画时长只有 0.4s延迟步长 300ms6 个字符总延迟 1.5s第一批字符已经结束一个完整周期最后几个字还没开始视觉上就像断了气。解决先定duration再按公式delay-step duration × 0.8 ÷ 字符数计算步长。以 6 个字符、1s 为例步长约 130ms。字符越多步长越小波纹越密反之字符少步长可以放宽让单个字符的运动更舒展。5.4 loading 结束时的闪灭display:none 是动画杀手现象接口返回后用 JS 将加载容器display: none整个面板瞬间消失动画在最后一帧戛然而止视觉上觉得“闪了一下”。原因display: none直接把这个元素从渲染树移除浏览器不会播放任何过渡动画生命周期被强杀。解决先做透明度过渡再真正移除。CSS 里给容器加一个隐藏类.loading-panel { transition: opacity 0.2s; } .loading-panel.hide { opacity: 0; }JS 等待过渡结束再清理 DOMpanel.classList.add(hide); panel.addEventListener(transitionend, () panel.remove(), { once: true });这样用户看到的是整体淡出而不是一个突然被剪断的画面。加载动画是给用户看的结束方式同样影响观感。5.5 translateY 位移把字符顶出容器overflow 遮挡与基线错位现象字符跳动时有下伸部的字母如 g、y、p 被裁掉一半或跳到最高处时字符之间开始重叠标题看起来脏兮兮的。原因父容器设置了overflow: hidden同时line-height太小translateY(-10px)让字符向上超出行框下伸部又被裁剪。逐字动画里如果没有给字符留出呼吸空间letter-spacing太小上浮的字符会和左右字符产生视觉碰撞。解决先给每个span加少量padding: 0 2px增大横向间距再让父容器line-height不小于font-size × 1.4。如果为了水波纹效果必须裁剪把overflow: hidden放在最外层包裹容器不要直接套在字符容器上。字符动效的边界检查最后要落到“最高位移 字体下伸部”这两个极限值上。6. 把动效接进真实业务状态切换、动画重启与DevTools调试这一节不聊新效果只聊动画怎么从演示变成生产可用。前三章的动效能不能落地取决于状态切换和调试手段。6.1 用 is-loading 类控制开关并用 reflow 重启动画业务里的加载状态应该是可切换的而不是进页面就一直播。我常用的结构是给根容器挂is-loading类动画只在这个类存在时运行div idloadingPanel classloading-panel is-loading rolestatus 正在加载 span classdot/spanspan classdot/spanspan classdot/span /div.loading-panel.is-loading .dot { display: inline-block; width: 4px; height: 4px; margin-left: 2px; border-radius: 50%; background: currentColor; animation: dot-blink 1.2s ease-in-out infinite; } .loading-panel:not(.is-loading) .dot { animation: none; }请求完成后移除类再配合上一章的淡出逻辑const panel document.getElementById(loadingPanel); fetch(/api/data) .then(() { panel.classList.add(hide); panel.addEventListener(transitionend, () panel.remove(), { once: true }); });如果同一个面板第二次被复用直接重新加is-loading类可能不会播放动画因为浏览器认为动画状态没有重置。强制一次回流就能解决panel.classList.remove(is-loading); void panel.offsetWidth; /* 强制 reflow重置动画状态 */ panel.classList.add(is-loading);逻辑说明读取offsetWidth会强制浏览器执行一次布局计算相当于告诉浏览器“这个元素状态变了”动画系统会重新初始化。这个技巧对“同一容器二次播放无反应”特别管用比删除重建 DOM 干净。6.2 DevTools 里把动画放慢十倍来调关键帧Chrome DevTools 的 Rendering 面板和 Animations 面板可以列出当前页面上所有运行中的动画。Animations 面板里能拖动时间线逐帧看也可以把播放速度调到 0.1 倍专门检查每个字符是否在正确时间点进入运动。另一个我常用的笨办法是暂停动画后逐帧检查中间状态。给正在调的元素加animation-play-state: paused然后在 Styles 面板里手动拖动关键帧百分比。这样能清楚看到30%处的位移是否穿帮、60%处的字符是否重叠不用反复刷新页面去赌某个瞬间长什么样。逐字动画最怕的是“整体看起来 OK但某一帧某一个字符卡了一下”放慢之后问题立刻现形。6.3 我的验证顺序先看状态再看动效我做这类动效时有一套固定验证顺序。第一是动画不抖动整个周期内字符位移连续没有回跳或瞬移第二是状态切换干净从 loading 到内容页没有闪灭DOM 里旧容器确实被移除第三是关闭动效后信息仍然完整在prefers-reduced-motion下文字可见读屏能读出“正在加载”。这三项都过了再看代码里的 delay 和 fill-mode。这些年做前端我最大的感受是加载动效不是“好看就行”而是给等待一个稳定的心理预期。每写一个新动效我都先想清楚它在什么状态下出现、什么状态下结束再动手写字符动画。节奏对了加载就不会让人觉得焦虑。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑