跳到主要内容HTML / CSS大前端
CSS 7球旋转加载动画:实现与避坑
用 HTML 和 CSS 实现一个 7 小球绕圈旋转的加载动画,核心技巧是 transform-origin 和 animation-delay 错开启动。文章展示了多种变体(跳跃、渐隐、色彩流动等),并分享了移动端性能优化、动画结束状态控制、CSS 变量管理等实际经验。适量加载动画能缓解等待焦虑,但超时控制和骨架屏才是终极方案。

加载动画说到底是为了让用户安心。点完按钮页面没反应,多半会以为挂了。放几个转圈的小球,至少能告诉对方'服务器还在,只是慢了点'。这种 7 球绕圈效果经典、轻量,而且兼容性没得说。
HTML 结构
别急着写 CSS,先把结构定清楚。用 ul 或 div 包 7 个子元素都行,但记得加无障碍标记。装饰性动画不需要被读屏软件反复念叨,给容器挂上 aria-hidden="true",再塞一个给屏幕阅读器看的文字说明,用 sr-only 藏起来就行。
<div class="loading-container" aria-label="正在加载数据" role="status">
<div class="loading-wrapper" aria-hidden="true">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div =>
加载中,请稍候...
class
"dot"
</div>
<div class="dot">
</div>
</div>
<span class="sr-only">
</span>
</div>
核心 CSS
核心就三个东西:transform-origin 定旋转中心,animation 控制节奏,@keyframes 画轨迹。要让小球看起来像接力,靠的是 animation-delay 错开启动。我把周期设成 1.4 秒,每个球延迟 0.2 秒,7 个刚好一圈。不是强制的,你按自己节奏调。
.loading-wrapper {
position: relative;
width: 60px;
height: 60px;
transform: translate3d(0, 0, 0);
}
.dot {
position: absolute;
width: 12px;
height: 12px;
background: #3498db;
border-radius: 50%;
transform-origin: 30px 30px;
transform: rotate(0deg) translateY(-25px);
animation: rotate 1.4s ease-in-out infinite;
}
.dot:nth-child(1) { animation-delay: 0s; }
.dot:nth-child(2) { animation-delay: 0.2s; }
.dot:nth-child(3) { animation-delay: 0.4s; }
.dot:nth-child(4) { animation-delay: 0.6s; }
.dot:nth-child(5) { animation-delay: 0.8s; }
.dot:nth-child(6) { animation-delay: 1.0s; }
.dot:nth-child(7) { animation-delay: 1.2s; }
@keyframes rotate {
0% {
transform: rotate(0deg) translateY(-25px) scale(1);
opacity: 1;
}
50% {
transform: rotate(180deg) translateY(-25px) scale(0.6);
opacity: 0.5;
}
100% {
transform: rotate(360deg) translateY(-25px) scale(1);
opacity: 1;
}
}
用 ease-in-out 比 linear 的机械感好很多,小球动起来更自然。
变体
同样的结构,稍微改改 @keyframes 就能变出不同风格。下面是我在不同场景下常用的几种,不一定全用到,知道原理就行。
跳跃式
.dot-jump {
position: absolute;
width: 10px;
height: 10px;
background: #e74c3c;
border-radius: 50%;
transform-origin: 30px 30px;
animation: jump 1.4s ease-in-out infinite;
}
@keyframes jump {
0%, 100% {
transform: rotate(0deg) translateY(-25px) scale(1);
}
25% {
transform: rotate(90deg) translateY(-35px) scale(1.3);
}
50% {
transform: rotate(180deg) translateY(-25px) scale(0.8);
}
75% {
transform: rotate(270deg) translateY(-30px) scale(1.1);
}
}
渐隐渐现
.dot-fade {
position: absolute;
width: 12px;
height: 12px;
background: #95a5a6;
border-radius: 50%;
transform-origin: 30px 30px;
animation: fade 1.2s linear infinite;
opacity: 0.2;
}
@keyframes fade {
0%, 100% {
opacity: 0.2;
transform: rotate(0deg) translateY(-25px) scale(1);
}
50% {
opacity: 1;
transform: rotate(180deg) translateY(-25px) scale(1.2);
}
}
颜色流动
用 CSS 变量或 hue-rotate() 让颜色跟着转动变化,多用于品牌色丰富的界面。
.dot-color {
position: absolute;
width: 12px;
height: 12px;
background: hsl(calc(var(--i) * 51), 70%, 50%);
border-radius: 50%;
transform-origin: 30px 30px;
animation: colorFlow 2s ease-in-out infinite;
}
@keyframes colorFlow {
0% {
transform: rotate(0deg) translateY(-25px);
filter: hue-rotate(0deg);
}
50% {
transform: rotate(180deg) translateY(-25px);
filter: hue-rotate(180deg);
}
100% {
transform: rotate(360deg) translateY(-25px);
filter: hue-rotate(360deg);
}
}
弹性拉伸
用 cubic-bezier 调出弹力感,游戏类页面很常见。
.dot-elastic {
position: absolute;
width: 10px;
height: 10px;
background: #e91e63;
border-radius: 50%;
transform-origin: 30px 30px;
animation: elastic 1.5s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite;
}
@keyframes elastic {
0% {
transform: rotate(0deg) translateY(-25px) scale(1);
}
25% {
transform: rotate(90deg) translateY(-25px) scaleX(1.5) scaleY(0.6);
}
50% {
transform: rotate(180deg) translateY(-25px) scaleX(0.6) scaleY(1.5);
}
75% {
transform: rotate(270deg) translateY(-25px) scaleX(1.3) scaleY(0.7);
}
100% {
transform: rotate(360deg) translateY(-25px) scale(1);
}
}
反向旋转
.dot-reverse:nth-child(odd) {
background: #3498db;
animation: rotateCW 2s linear infinite;
}
.dot-reverse:nth-child(even) {
background: #2ecc71;
animation: rotateCCW 2s linear infinite;
}
@keyframes rotateCW {
from { transform: rotate(0deg) translateY(-25px); }
to { transform: rotate(360deg) translateY(-25px); }
}
@keyframes rotateCCW {
from { transform: rotate(360deg) translateY(-25px); }
to { transform: rotate(0deg) translateY(-25px); }
}
大小交替
.dot-breathe:nth-child(odd) {
background: #9b59b6;
animation: breatheBig 1.4s ease-in-out infinite;
}
.dot-breathe:nth-child(even) {
background: #f39c12;
animation: breatheSmall 1.4s ease-in-out infinite;
}
@keyframes breatheBig {
0%, 100% {
transform: rotate(0deg) translateY(-25px) scale(1.5);
opacity: 0.8;
}
50% {
transform: rotate(180deg) translateY(-25px) scale(0.8);
opacity: 0.4;
}
}
@keyframes breatheSmall {
0%, 100% {
transform: rotate(0deg) translateY(-25px) scale(0.6);
opacity: 0.4;
}
50% {
transform: rotate(180deg) translateY(-25px) scale(1.2);
opacity: 0.9;
}
}
暂停呼吸
.dot-pause {
position: absolute;
width: 12px;
height: 12px;
background: #1abc9c;
border-radius: 50%;
transform-origin: 30px 30px;
animation: rotate 1.4s ease-in-out infinite;
animation-play-state: running;
transition: transform 0.3s ease;
}
.loading-wrapper:hover .dot-pause {
animation-play-state: paused;
transform: scale(1.2) !important;
}
碰过的坑
移动端卡顿:低端安卓机上,filter 和复杂 transform 容易触发重绘。我习惯给容器加 transform: translate3d(0, 0, 0),强制硬件加速。will-change 也能用,但用完记得取消,不然内存会慢慢涨上去。
动画结束后小球停得歪:加载完了 JS 把 loading 移除,小球可能卡在半空。这时候 animation-fill-mode: forwards 管用,提前设好结束状态。
小球对不齐:别手动调 margin,直接用 flex 或 grid 居中。
.loading-wrapper {
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
颜色难统一:把颜色、大小、时长抽成 CSS 变量,一处改全换,深色模式适配也方便。
:root {
--dot-base-color: #3498db;
--dot-size: 12px;
--animation-duration: 1.4s;
}
.dot {
width: var(--dot-size);
height: var(--dot-size);
background: var(--dot-base-color);
animation-duration: var(--animation-duration);
}
上线前的自查
- 容器一定挂到特定 div 下,别直接放 body;路由跳转时记得销毁,不然动画在后台一直跑。
- 避免 FOUC:默认隐藏,等 JS 准备好再淡入。
- 设最大显示时间,超过 10 秒最好给个超时提示,别让用户傻等。
- 深色模式下背景色可能吞掉小球,要么用
currentColor,要么在媒体查询里换个亮色。
- 不要一上来就显示 loading,接口响应 300ms 以内的时候闪一下反而不舒服。先用变量记一下时间,超了再展示。
说到底,loading 只是缓兵之计,接口慢得离谱的话加个骨架屏或分批加载才是正经。
再加点花样
随机延迟:用 calc() 加一点偏移,比死板的 0.2 秒更生动。
.dot:nth-child(2) {
animation-delay: calc(var(--base-delay) * 1 + 0.05s);
}
hover 炸开:配合 transition 比纯 animation 更柔和,小球悬停时散开。
.dot-interactive {
transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.loading-wrapper:hover .dot-interactive:nth-child(1) {
transform: rotate(0deg) translateY(-40px) scale(1.5);
}
文字提示联动:如果还要带'加载中...'的省略号动画,可以这么玩。
.loading-text::after {
content: '...';
animation: dots 1.5s steps(4, end) infinite;
}
@keyframes dots {
0%, 20% { content: '.'; }
40% { content: '..'; }
60% { content: '...'; }
80%, 100% { content: ''; }
}
基本就是这些。前端动画很多时候是细节堆出来的,没必要一次性全用上,挑适合自己项目的就好。
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online