跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
HTML / CSS大前端

CSS3 双半圆进度条实战:拒绝 JS 也能丝滑旋转

本文详解如何使用 CSS3 结合少量 JavaScript 实现双半圆进度条。核心思路是利用两个绝对定位的半圆容器,通过 clip-path 裁剪和 transform 旋转模拟进度变化。超过 50% 时切换层级与旋转逻辑,配合 CSS 变量动态控制角度。方案性能优于 SVG,兼容主流浏览器,但需注意 Safari 边缘渲染问题及复杂场景下的维护成本。适合单色环形加载动画等轻量级需求。

极客工坊发布于 2026/4/5更新于 2026/6/716 浏览
CSS3 双半圆进度条实战:拒绝 JS 也能丝滑旋转

CSS3 双半圆进度条实战:拒绝 JS 也能丝滑旋转

说实话,我到现在还记得三年前那个暴雨夜。老板拍着我肩膀说:"小王啊,这个圆形进度条,不要搞那些花里胡哨的 SVG,也不要 Canvas,就用纯 CSS,要轻量,要优雅,要丝滑。"我当时年轻啊,以为就是个圆环嘛,能有多难?结果这一脚踩进去,直接让我怀疑人生到凌晨三点。

市面上搜出来的教程,十个有九个是教你用 SVG 的 stroke-dasharray,剩下那个是 Canvas 画弧。我就奇了怪了,CSS3 明明有 border-radius,有 transform,有 clip-path,怎么就没人好好讲讲"两个半圆拼圆环"这种原生方案?难道大家都觉得这种写法太野路子,上不了台面?

今天必须把这事掰扯清楚。不是为了炫技,就是单纯咽不下这口气。而且我发誓,这次一定要写得比那些 AI 生成的教程像人话——毕竟当年被坑的时候,看着那些"首先我们需要创建一个容器"的八股文,我真的想把键盘砸了。

先别急着写代码,咱们聊聊这俩半圆到底怎么"打架"

很多人一看到圆形进度条就怂,觉得要什么三角函数、弧度计算。其实吧,真没那么玄乎。核心思路土得掉渣:左边藏一个半圆,右边藏一个半圆,让它们转起来互相遮挡,露出来的部分就是进度。

想象一下,你手里有两个半圆形的饼干,把它们背靠背拼在一起,正好是个圆。现在左边的饼干固定不动,右边的饼干开始顺时针转。当右边转到 180 度时,它正好把左边完全盖住,这时候进度就是 50%。如果继续转,右边的饼干会跑到左半边去,这时候就需要左边那个"卧底"出来接力了。

这逻辑听起来像不像两个人在抢椅子?前 50% 右边那个人疯狂表演,后 50% 换左边的人上场。这就是为什么纯 CSS 方案处理 0-50% 和 50-100% 要用两套逻辑——因为超过 180 度后,你光靠一个半圆转是盖不住整个圆的,必须左右切换。

那个旋转角度计算,其实就是小学数学:360 度对应 100%,所以 1% 是 3.6 度。想要显示 30% 的进度?右边转 30 * 3.6 = 108deg 就行了。但如果想要 70% 呢?右边先转到 180 度(也就是 rotate(180deg)),然后左边那个从 0 度开始转,70 - 50 = 20%,20 * 3.6 = 72deg,所以左边转 72 度。就这么简单,别被那些动不动就搬出π和弧度的教程吓到了。

这里有个坑我得提前说:很多人搞不清 transform-origin 到底该设在哪。如果你让右边的半圆绕着它自己的中心转,那它只会原地打转,根本盖不住左边。所以必须把旋转中心设在圆的直径上——对于右半圆来说,就是 left: 0,也就是整个圆的中心点。这样它转起来才会像扇子的合页一样,沿着直径扫过去。

HTML 结构:大道至简,三个 div 走天下

我见过有人为了这个进度条嵌套五六个 div,还美其名曰"语义化"。语义化个鬼啊,这就是个视觉组件,又不是给盲人读屏用的。我的原则是:能少一层绝不多一层,层级越深,后期调试越想死。

最精简的结构长这样:

<!-- 外层容器:定宽高,当 clip-path 用 -->
<div class="progress-circle" style="--percent: 75;">
  <!-- 左半圆:负责 50%-100% 的阶段 -->
  <div class="half-container left">
    <div class="progress-bar"></div>
  </div>
  
  
    
  
  
  

<!-- 右半圆:负责 0%-50% 的阶段 -->
<div class="half-container right">
<div class="progress-bar">
</div>
</div>
<!-- 中间遮罩:搞成空心圆环的关键 -->
<div class="center-mask">
</div>
</div>

看到没?就三个 div。.half-container 包裹了两个绝对定位的半圆,.center-mask 是用来把中间掏空的——如果你想要的是实心饼图,这层可以删掉。

等等,我好像漏说了什么。对,CSS 变量。现在都用 2026 年了,别再用 JS 去算角度然后写内联样式了,直接用 CSS 自定义属性,JS 只需要改一个 --percent 的值,剩下的交给 CSS 自己算。这才是现代前端该有的样子:

/* 核心逻辑:JS 只改变量,CSS 负责计算 */
.progress-circle {
  --percent: 0;
}

这样后面配合 JS 的时候,只需要 element.style.setProperty('--percent', 当前值),比操作 class 或者直接改 transform 优雅一万倍。

CSS 魔法:让这两个半圆听话地转起来

先上基础样式,把圆环的骨架搭起来。这里的关键是 overflow: hidden 和 border-radius: 50% 的配合,前者负责切掉多余的半圆,后者负责让容器变圆。

.progress-circle {
  /* 圆环大小,想改多大改多大 */
  width: 200px;
  height: 200px;
  /* 圆形容器,超出部分全部咔嚓掉 */
  border-radius: 50%;
  position: relative;
  /* 背景色就是未填充部分的颜色,比如灰色 */
  background: #e0e0e0;
  /* 这个很重要,后面的旋转基准点 */
  --percent: 0;
}

/* 两个半圆容器,用来控制显示范围 */
.half-container {
  position: absolute;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  overflow: hidden;
}

/* 右半圆容器只显示右边 50% */
.half-container.right {
  clip-path: inset(0 0 0 50%);
}

/* 左半圆容器只显示左边 50% */
.half-container.left {
  clip-path: inset(0 50% 0 0);
}

/* 实际的旋转元素 */
.progress-bar {
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
  transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}

/* 右半圆:0-50% 时旋转,超过 50% 固定 180 度 */
.half-container.right .progress-bar {
  transform-origin: left center;
  transform: rotate(calc(min(var(--percent), 50) * 3.6deg));
}

/* 左半圆:0-50% 时隐藏(-180 度),超过 50% 后开始旋转 */
.half-container.left .progress-bar {
  transform-origin: right center;
  transform: rotate(calc(max(var(--percent) - 50, 0) * 3.6deg - 180deg));
}

看到这段 CSS 你可能有点懵,特别是那个 calc 里面套 min 和 max 的操作。这是 CSS 比较函数(CSS Comparison Functions),2020 年之后的浏览器都支持了。它的作用是:不需要 JS,CSS 自己就能判断当前进度在哪个区间,该转多少度。

让我拆解一下右半圆的 transform:

  • 当 --percent 是 30 时,min(30, 50) 等于 30,所以转 30 * 3.6 = 108deg,显示右半边的一部分
  • 当 --percent 是 70 时,min(70, 50) 等于 50,所以转 50 * 3.6 = 180deg,右半圆完全显示

左半圆的逻辑稍微绕一点:

  • 当 --percent 是 30 时,max(30, 50) 等于 50,所以 (50 - 50) * 3.6 - 180 = -180deg,左半圆完全隐藏在左边
  • 当 --percent 是 70 时,max(70, 50) 等于 70,所以 (70 - 50) * 3.6 - 180 = 72 - 180 = -108deg,左半圆从隐藏状态转出

空心圆环的秘密:中间挖个洞就完事了

上面代码里的 .center-mask 还没写样式。这其实更简单,就是一个绝对定位的圆形,背景色跟页面背景一样,盖在最中间,营造出"只有边框在动"的效果。

.center-mask {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 160px; /* 比外圆小 40px,就是 20px 的环宽 */
  height: 160px;
  background: white; /* 跟页面背景色一致 */
  border-radius: 50%;
  z-index: 10; /* 必须盖在最上面 */
  /* 如果想加点阴影提升质感 */
  box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
}

如果你想要的是实心饼图(像那种磁盘空间占用图),直接把 .center-mask 删掉就行。或者把它的背景色设成半透明,能看到后面的内容,那就是另一种风格了。

这里有个细节:如果父容器有背景图而不是纯色背景,.center-mask 的背景色就不能写死成 white 了,得用 background: inherit 继承父容器的背景,或者用 backdrop-filter: blur(10px) 搞毛玻璃效果。不过后者性能开销比较大,移动端慎用。

动效调教:怎么转才不会像抽风

很多人写 transition 就写个 all 0.3s ease,太敷衍了。进度条这种需要精确控制的动画,得用 cubic-bezier 自定义贝塞尔曲线。我上面用的 cubic-bezier(0.4, 0, 0.2, 1) 是 Material Design 的标准缓动曲线,特点是开始快中间慢结束快,看起来比较自然。

如果你想要那种"咔哒咔哒"的机械感,可以用 steps(10),让进度条一格一格地跳。如果是加载中的无限循环,那就不能用 transition 了,得用 @keyframes:

@keyframes rotate {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}
.loading .progress-bar {
  animation: rotate 1s linear infinite;
  transition: none; /* 这时候 transition 要去掉,不然会打架 */
}

还有个坑是关于旋转方向的。CSS 的旋转默认是顺时针,但有时候你会发现进度条从 100% 回到 0% 的时候,它不是直接缩回去,而是顺时针转一大圈(经过 360 度)回到 0 度,看起来就像抽风一样。这是因为浏览器觉得从 350 度到 10 度,顺时针只要转 20 度,逆时针要转 340 度,所以选了顺时针——但它不知道你是想"倒退"进度啊!

解决办法是确保角度计算不会出现这种"绕远路"的情况。比如用 JS 控制的时候,如果新值比旧值小,先让旧值减 360 度,这样浏览器就会选择逆时针方向了。或者干脆用 transition: none 瞬间跳过去,然后再开动画。

真实世界的翻车现场

Safari 的 1px 毛边惨案

你以为代码写完就完事了?Too young。在 Safari 上,两个半圆的接缝处会出现一条细细的白线或者黑线,特别是当进度接近 50% 的时候。这是因为浏览器的抗锯齿算法在边缘处理上有点抽风,两个图形拼接的地方没有完全对齐。

我的解决方案是:让两个半圆稍微重叠 1px。把右半圆的宽度设成 calc(50% + 1px),左半圆也往右偏移 1px。这样它们就有 1px 的重叠区域,接缝处的缝隙就被盖住了。虽然理论上会有 1px 的颜色叠加,但因为进度条是纯色,叠加后也没啥明显变化。

.half-container.right {
  clip-path: inset(0 0 0 50%);
  width: calc(50% + 1px); /* 偷偷加 1px */
}
.half-container.left {
  clip-path: inset(0 50% 0 0);
  left: -1px; /* 往左挪 1px,跟右边重叠 */
  width: calc(50% + 1px);
}

动态修改时的闪烁黑线

有时候用 JS 动态改百分比,特别是在 50% 那个临界点切换的时候,会出现一瞬间两个半圆都显示或者都隐藏的闪烁。这是因为 DOM 更新和样式计算有个时间差,浏览器先渲染了 z-index 变化,还没来得及算新的 transform。

我的土办法是:在修改百分比前,先把 transition 关掉,改完后再打开。或者更简单粗暴的,给容器加 will-change: transform,让浏览器提前把图层准备好。

// 修改进度前的准备
circle.style.transition = 'none';
circle.style.setProperty('--percent', 新值);
// 强制重绘,让上面的改动立即生效
circle.offsetHeight;
// 恢复动画
circle.style.transition = 'transform 0.6s cubic-bezier(0.4, 0, 0.2, 1)';

transform 坐标系错乱

如果你的进度条父容器或者祖先元素也有 transform 属性(比如做了缩放、旋转),那子元素的 transform-origin 就会基于那个被变换过的坐标系,而不是原来的视口坐标系。结果就是旋转中心跑偏,进度条转得歪七扭八。

这问题没有完美的 CSS 解决方案,要么确保进度条的容器不要有 transform 祖先,要么用 JS 实时计算正确的旋转中心。或者换个思路,不用 transform: rotate,改用 clip-path: polygon() 来切出扇形,虽然计算复杂点,但不受 transform 层级影响。

让同事喊 666 的骚操作

技巧一:空心实心一键切换

给容器加个 data-type 属性,CSS 里用属性选择器判断:

/* 实心饼图模式 */
.progress-circle[data-type="pie"] .center-mask {
  display: none;
}
/* 圆环模式(默认) */
.progress-circle[data-type="ring"] .center-mask {
  display: block;
}

JS 只需要改个属性:circle.setAttribute('data-type', 'pie'),瞬间从加载动画变成磁盘占用图。

技巧二:阴影提升质感

别小看 box-shadow,用好了能让你的进度条从"程序员画的"变成"设计师画的"。外发光加内阴影,层次感就出来了:

.progress-circle {
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06), inset 0 2px 4px rgba(0,0,0,0.05);
}
.center-mask {
  box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
}

技巧三:封装成 Mixin

如果你用 Sass/Less,把这个逻辑封装起来,下次复制粘贴:

@mixin circle-progress($size: 200px, $stroke: 20px, $color: #667eea) {
  width: $size;
  height: $size;
  border-radius: 50%;
  position: relative;
  background: #e0e0e0;
  
  .half-container {
    position: absolute;
    width: 100%;
    height: 100%;
    border-radius: 50%;
    overflow: hidden;
    &.right { clip-path: inset(0 0 0 50%); }
    &.left { clip-path: inset(0 50% 0 0); }
  }
  
  .progress-bar {
    width: 100%;
    height: 100%;
    background: $color;
    transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
  }
  
  .center-mask {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: $size - ($stroke * 2);
    height: $size - ($stroke * 2);
    background: white;
    border-radius: 50%;
  }
}
// 使用
.my-progress { @include circle-progress(150px, 15px, linear-gradient(90deg, #f093fb 0%, #f5576c 100%)); }

这方案到底香不香?

说实话,写到这我都累了。这套方案优点很明显:零 JS 依赖(如果不算那行改 CSS 变量的代码),性能比 SVG 操作 DOM 节点快,比 Canvas 轻量,老浏览器(IE10+)勉强能跑。

但缺点也很致命:超过 50% 后的逻辑确实反人类,我上面写的那些 minmax 计算,过三个月我自己都看不懂。而且如果你想搞那种多色分段进度条(比如 0-30% 红色,30-70% 黄色,70-100% 绿色),这套方案直接 gg,因为两个半圆都是单色的,换颜色就得换元素,复杂度爆炸。

还有,响应式适配比较麻烦。SVG 可以 viewBox 自适应,Canvas 可以重绘,CSS 这个方案如果容器大小变了,你得重新算 border-radius 的像素值(虽然用百分比可以部分解决,但 border-radius: 50% 和 clip-path 的百分比基准不一样,容易出 bug)。

所以我的建议是:简单场景用它,复杂场景老老实实 SVG。加载动画、单色的圆形进度条、不需要精确控制每一段颜色的,这套方案够用了。如果是仪表盘、需要渐变色的、要分段显示的,别硬撑,上 SVG 或者 ECharts 吧。

最后的碎碎念

写到这我看了眼字数,好像快五千了?其实还可以继续水,比如讲讲怎么用 conic-gradient 实现更简单的方案(虽然兼容性差点),或者怎么用 mask-image 搞更骚的操作。但算了,留点头发要紧。

说实话,现在回头看,当年那个暴雨夜让我死磕纯 CSS,可能纯粹是老板不懂技术瞎指挥。但正是那次折磨,让我对 CSS 的 transform、clip-path、overflow 这些属性有了肌肉记忆。有时候前端就是这样,为了省几行 JS 代码,CSS 能写出花来,行数能绕地球一圈。

下次产品再提这种"就要纯 CSS 不要 JS"的需求,我建议你直接把这篇文章甩他脸上,让他看看这玩意儿有多难搞。如果他坚持,那就把预估工时乘以 3,毕竟头发的损失得用加班费来弥补。

如果调了半天还是调不出来,别硬撑。引入个 progressbar.js 或者 nanobar 不丢人,真的。毕竟咱们是解决问题,不是炫技。发际线要紧,心情要紧,下班后的火锅更要紧。

目录

  1. CSS3 双半圆进度条实战:拒绝 JS 也能丝滑旋转
  2. 先别急着写代码,咱们聊聊这俩半圆到底怎么"打架"
  3. HTML 结构:大道至简,三个 div 走天下
  4. CSS 魔法:让这两个半圆听话地转起来
  5. 空心圆环的秘密:中间挖个洞就完事了
  6. 动效调教:怎么转才不会像抽风
  7. 真实世界的翻车现场
  8. Safari 的 1px 毛边惨案
  9. 动态修改时的闪烁黑线
  10. transform 坐标系错乱
  11. 让同事喊 666 的骚操作
  12. 技巧一:空心实心一键切换
  13. 技巧二:阴影提升质感
  14. 技巧三:封装成 Mixin
  15. 这方案到底香不香?
  16. 最后的碎碎念
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • CleanShot X Mac 截图录屏及 GIF 录制完整指南
  • 为何最终我放弃了 Go 的 sync.Pool
  • C++ 结构体基础:定义、成员函数与排序
  • ChatGPT 免费版与微软 Copilot 技术对比与选型指南
  • QTTabBar 为 Windows 资源管理器添加标签页功能
  • Gazebo 机器人三维物理仿真平台
  • 字节跳动交易与广告前端一面面经深度解析
  • Stable-Diffusion-v1-5 镜像交付标准:Dockerfile 透明与 SHA256 校验
  • Ollama v0.17.0 发布:OpenClaw 自动安装、Web 搜索及 Context 动态分配优化
  • 工程项目管理系统技术架构与核心功能梳理
  • Python Flask RESTful API 开发指南与项目结构模板
  • Git Windows 安装与核心配置详解
  • 前端Canvas:让你的网站更具视觉冲击力
  • 蚂蚁金服 AIGC 产品经理面试经验与复盘
  • Java Web 学习:前端 HTML 核心知识点总结
  • Rust 环境搭建与配置实战指南
  • Linux 常见指令(下)
  • RocketMQ Java 生态消息中间件实战详解
  • Supabase 全栈开发实战:数据库、认证与本地部署
  • 剪映 AI 辅助影视解说自动化工作流实战指南

相关免费在线工具

  • 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