SVG <circle> + stroke-dasharray 是最可控的环形进度条方案,通过周长计算、stroke-dashoffset 动态控制进度,配合 stroke-linecap="round" 消除接缝,支持高精度缩放与平滑动画。

用 SVG <circle> + stroke-dasharray 实现最可控的环形进度条
纯 CSS 方案(比如用 border-radius: 50% 配合旋转遮罩)看起来简单,但实际很难精确控制进度、响应式缩放易错、动画卡顿常见。SVG 是目前最稳妥的选择,核心靠 <circle> 的描边路径长度和虚线绘制逻辑来模拟“画圆”过程。
关键点在于:先算出圆周长 2 * π * r,再用 stroke-dasharray 设定“实线段长度 = 当前进度 × 周长”,stroke-dashoffset 控制起始绘制位置(通常设为负值让进度从顶部开始)。
示例(r=50,viewBox="0 0 100 100"):
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45"
fill="none"
stroke="#e0e0e0"
stroke-width="8"/>
<circle cx="50" cy="50" r="45"
fill="none"
stroke="#4CAF50"
stroke-width="8"
stroke-dasharray="282.74" <!-- 2 * π * 45 ≈ 282.74 -->
stroke-dashoffset="282.74" <!-- 初始全空 -->
style="transition: stroke-dashoffset 0.3s ease-out;" />
</svg>JS 更新进度时只需动态改 stroke-dashoffset:
立即学习“前端免费学习笔记(深入)”;
const progressCircle = document.querySelector('svg circle:last-child');
const circumference = 282.74;
const targetPercent = 75;
progressCircle.style.strokeDashoffset = circumference - (targetPercent / 100) * circumference;为什么不能直接用 transform: rotate() 模拟进度
有人尝试用一个半圆 <div> 旋转 + overflow 遮罩,或两个 180° 扇形拼接——这类方案在进度接近 0% 或 100% 时极易出现视觉撕裂、角度计算误差(尤其涉及小数精度)、CSS 动画 easing 不匹配真实进度曲线。
更麻烦的是:旋转中心偏移、父容器 transform 影响、高 DPI 屏幕下锯齿明显。而 SVG 路径绘制是矢量原生支持,缩放不失真,动画由浏览器渲染管线统一调度,稳定性高得多。
将调研报告、方案对比、数据分析、需求梳理、会议纪要、设计说明、代码评审、学习笔记等任务产物输出为可在浏览器直接打开的单文件 HTML,替代 Markdown 作为最终交付物。Markdown 在信息密度、视觉清晰度和可分享性上已跟不上 AI 产物的丰富度,HTML 能同时承载排版、表格、SVG 等。
常见翻车点:
- 用
deg计算旋转角度但没考虑起始方向(默认 0° 在右侧,环形进度习惯从顶部开始) - 没处理 0% → 1% 和 99% → 100% 的过渡跳变(CSS 动画插值在极值附近容易突变)
- 未设置
transform-origin: center导致旋转轴心偏移
stroke-linecap: round 能解决接头毛刺问题
当进度不是整圆时,<circle> 的起点和终点会暴露尖锐接缝,尤其在粗描边(stroke-width > 6)下非常明显。加一句 stroke-linecap="round" 就能让两端自动变成半圆帽,视觉上严丝合缝。
注意:这个属性只对描边生效,不影响填充;且必须加在带 stroke-dasharray 的那个 <circle> 上,否则无效。
其他实用配置项:
-
stroke-linejoin="round":如果后续要扩展成多段弧(比如不同颜色进度),防止连接角出现棱角 -
shape-rendering="geometricPrecision":在需要高精度路径(如金融仪表盘)时启用,避免浏览器自动优化导致微小偏移 - 避免在
<circle>上用fill,哪怕设为none,也比留空更稳妥(某些旧版 Safari 对空 fill 处理异常)
React/Vue 中封装要注意 props 变化时的动画重置
组件接收 percent prop 后,不能每次更新都直接设 stroke-dashoffset —— 如果新旧值相同,浏览器可能跳过重绘;如果值突变(比如从 10 → 80),需确保 transition 属性始终存在且未被内联样式覆盖。
推荐做法:
- 用
useEffect(React)或watch(Vue)监听percent,计算新offset后强制触发 reflow(例如读取getBoundingClientRect())再设值 - 把
transition写在 CSS 类里,而不是内联 style,避免被 JS 覆盖 - 初始渲染时若
percent === 0,stroke-dashoffset应设为最大值(即周长),而非 0 —— 否则会显示满圈
真正难的不是画出来,而是让 0.1% 的变化在 retina 屏上也平滑、不跳、不糊。SVG 路径方案在这点上容错率高,但每个数值(r、stroke-width、dasharray)之间都有隐含比例关系,动一个就得同步调另外两个。


















