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

两个半圆转出圆形进度条:纯CSS方案与踩坑全记录

用两个半圆互相遮挡,纯CSS就能做出圆形进度条,不用写一行JS。只要搞清楚左右半圆在0-50%和50-100%时的旋转配合,借助min/max函数自动计算角度,就能实现丝滑动画。文章拆解了HTML结构、关键CSS、空心环做法,还聊了Safari毛边、动态修改闪烁这些坑,以及阴影、mixin封装等实用技巧。最后给了适用场景判断:简单的用它,复杂的上SVG。

极客工坊发布于 2026/6/25更新于 2026/10/1868 浏览
两个半圆转出圆形进度条:纯CSS方案与踩坑全记录

两个半圆转出圆形进度条:纯CSS方案与踩坑全记录

三年前那个暴雨夜,老板让我用纯CSS搞一个圆形进度条,不要SVG不要Canvas。我起初觉得不难,结果搞到凌晨三点。后来发现,网上大部分教程不是用SVG的stroke-dasharray,就是Canvas画弧,很少有人讲用两个半圆拼圆环的原生CSS办法。今天把这事说清楚——不是为了炫技,就是咽不下这口气,也保证写得比那些'首先我们需要创建一个容器'的AI八股文像人话。

这俩半圆怎么'打架'?

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

想象手里有两个半圆饼干,背靠背拼成一个圆。左边那块不动,右边那块顺时针转。转到180度时,右边刚好盖住左边,进度就是50%。再继续转,右边会跑到左半边去,这时左边的'卧底'要出来接力。这就是为什么纯CSS方案处理0‑50%和50‑100%要用两套逻辑——因为超过180度后,光靠一个半圆转盖不住整个圆,必须左右切换。

角度计算很简单:360度对应100%,1%就是3.6度。显示30%时,右边转30×3.6=108deg就行。70%呢?右边先转到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>

.half-container包裹了两个绝对定位的半圆,.center-mask用来把中间掏空——如果你要实心饼图,这层可以删掉。另外,用CSS自定义属性--percent控制进度,JS只改这一个变量,角度计算全交给CSS,比操作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));
}

你可能对calc里面套min和max有点懵,这是CSS比较函数,2020年后浏览器基本都支持。它的作用就是让CSS自己判断当前进度在哪个区间,该转多少度,完全不用JS插一脚。

右半圆那段逻辑拆开看:

  • --percent: 30 → min(30,50) = 30 → 转108deg,只显示右半边的30%
  • --percent: 70 → min(70,50) = 50 → 转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,左半圆从隐藏状态转出了72度

空心圆环的秘密:中间挖个洞

.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。如果父容器是背景图,背景色就不能写死,得用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,否则冲突 */
}

还有一个坑:旋转方向。进度从100%退回0%时,有时进度条不是直接缩回去,而是顺时针转一大圈(经过360度)回到0度,像抽风一样。这是因为浏览器选了顺时针最短路径,但它不知道你想倒退。解决办法是确保角度计算不出现这种情况,或者在JS里检查新旧值,新值小于旧值就先让旧值减360度,再用transition: none瞬间跳过去再开动画。

真实世界的翻车现场

Safari的1px毛边

Safari上两个半圆接缝处会冒出一条细线,进度接近50%时尤其明显。把右半圆宽度设成calc(50% + 1px),左半圆也往右偏移1px,让它们重叠,接缝就被盖住了。

.half-container.right {
  clip-path: inset(0 0 0 50%);
  width: calc(50% + 1px);
}
.half-container.left {
  clip-path: inset(0 50% 0 0);
  left: -1px;
  width: calc(50% + 1px);
}

动态修改时的闪烁黑线

JS修改百分比,尤其在50%临界点切换时,可能一闪两个半圆都出现或都消失。这是因为DOM更新和样式计算有时间差。我的土办法:先关掉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会基于被变换过的坐标系,旋转中心跑偏。要么确保容器没有transform祖先,要么用JS实时计算旋转中心,或者干脆不用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封装,下次直接@include:

@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%绿),这套方案直接歇菜——两个半圆都是单色,换颜色就得加元素,复杂度爆炸;响应式适配也很麻烦,SVG可以viewBox自适应,Canvas能重绘,CSS方案容器大小一变,你得重新算像素值,border-radius:50%和clip-path的百分比基准还不一样,容易出bug。

所以我的建议是:简单场景用它,复杂场景老老实实SVG。加载动画、单色圆形进度条、不需要分段颜色的,这套够用。仪表盘、带渐变色、分段显示的,别硬撑,上SVG或ECharts。

最后碎碎念

写到这里,其实还可以用conic-gradient实现更简单的方案(虽然兼容性差点),或者用mask-image搞更骚的操作,但留点头发要紧。

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

下次产品再说'就要纯CSS不要JS',可以直接把这篇文章甩他脸上,让他看看这东西有多难搞。如果他坚持,就把预估工时乘以3,头发的损失得用加班费补回来。调不出来也别硬撑,引入progressbar.js或nanobar不丢人,解决问题才是关键,发际线和心情更重要。

目录

  1. 两个半圆转出圆形进度条:纯CSS方案与踩坑全记录
  2. 这俩半圆怎么“打架”?
  3. HTML结构:三个div就够了
  4. CSS魔法:让半圆听话地转
  5. 空心圆环的秘密:中间挖个洞
  6. 动画调校:怎么转不抽风
  7. 真实世界的翻车现场
  8. Safari的1px毛边
  9. 动态修改时的闪烁黑线
  10. transform坐标系错乱
  11. 让同事喊666的骚操作
  12. 这方案到底香不香?
  13. 最后碎碎念

更多推荐文章

查看全部
  • Windows 系统安装 RabbitMQ 完整指南
  • 17:无人机远程执行路径规划:A*算法与GPS精准打击
  • Neo4j 安装与使用教程
  • Gemma-3-12B-IT WebUI 部署与使用指南
  • OpenClaw Gateway Windows 开机自启与 Dashboard 自动打开方案
  • 解决 NVIDIA RTX 50 系列 (sm_120) 架构下的 PyTorch 与 Unsloth 依赖冲突
  • C/C++ 错误处理机制与常用函数
  • Docker 快速部署 AstrBot+NapCat 打造 QQ 智能客服机器人
  • 利用 Python 进行网络接单与兼职赚钱的渠道分析
  • LLM 模型微调:PEFT 与 QLoRA 技术总结
  • AirSim 无人机仿真入门:实现起飞与降落控制
  • 无需插件修改 Copilot 接入第三方 OpenAI 接口方法
  • 2025 主流 AI 编程工具定价对比:Cursor、Windsurf、Kiro、Zed 与 VS Code
  • RabbitMQ 中 Ready 和 Unacked 的含义及系统健康判断
  • DooTask 如何利用轻量化与 AI 技术优化开发团队协同管理
  • GLM 语言模型拆解:从概率图到 PyTorch 代码
  • PyCharm 中 Copilot 插件 Claude 模型缺失的解决方案
  • CATE 条件平均处理效应估计:五大方法原理详解与 Python 实战
  • 大语言模型提示词工程核心技巧与实践指南
  • llama.cpp 重大更新:内置 Web UI,性能对比 Ollama

相关免费在线工具

  • 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