先给结论:静态展示用 conic-gradient 一行就能画出圆环进度条;要做平滑动画、或者在旧浏览器上也要一致,选 SVG 的 stroke-dasharray 方案;border 旋转遮罩是兼容老浏览器的兜底写法,代价是结构复杂。

同一个圆环进度条能有三四种写法,是因为 CSS 里"画一段圆弧"这件事本来就没有唯一答案:可以用渐变、可以用边框、也可以干脆交给 SVG。选错写法的代价通常不在能不能画出来,而在后面加动画、改颜色、适配旧浏览器时反复返工。
本文先给一个能直接跑的最小例子,再逐个给出三种写法的完整代码与适用边界,最后用一张对照表和一份验证清单收尾。示例不依赖任何框架。
先看结论
- 只想静态显示百分比:conic-gradient 最短,改一个角度值就能换进度。
- 要动画或要跨浏览器像素级一致:SVG 描边方案最稳,进度就是一个数值属性。
- 需要兼容不支持渐变的旧浏览器:border 加伪元素旋转遮罩,能画但要接受结构变复杂。
渐变的语法参数不少,写不顺时可以对照 CSS 教程 里按属性分组的清单查。
一、最小场景与预期行为
把任务定死:一个直径 120 像素的圆环,环宽 12 像素,底色浅灰,进度 68%,圆心显示百分比数字。
输入是一段 HTML 加 CSS。预期行为是:改一个数值就能改变进度,圆心文字与进度保持同步,环的两端是平口(不是圆头),缩放容器时圆环不变形。
<div class="ring" style="--p: 68">
<span class="ring-text">68%</span>
</div>
.ring {
width: 120px;
height: 120px;
border-radius: 50%;
background: conic-gradient(#fe6a00 calc(var(--p) * 1%), #ececec 0);
display: grid;
place-items: center;
}
.ring::before {
content: "";
position: absolute;
width: 96px;
height: 96px;
border-radius: 50%;
background: #fff;
}
这段做的是:用 conic-gradient 从 0 度开始画一段橙色扇形到指定百分比,剩余部分填浅灰;再用一个白色圆形盖在中间挖出环心。百分比通过一个自定义属性 --p 传入,改数字就换进度。
关键点有两个:一是 calc(var(--p) * 1%) 把无单位数字换算成百分比,二是圆心那块白色遮罩需要父元素有定位上下文,别忘了给 .ring 补 position: relative。
二、先用最省事的写法:conic-gradient
conic-gradient 是角向渐变,颜色沿圆周分布,天生适合画圆环进度条。
.ring {
position: relative;
width: 120px;
height: 120px;
border-radius: 50%;
background: conic-gradient(
#fe6a00 0turn calc(var(--p) / 100 * 1turn),
#ececec calc(var(--p) / 100 * 1turn) 1turn
);
}
这段做的是把百分比换算成 turn 单位,明确写出橙段的起止角度与灰段的起止角度。比上一版更严谨:显式写出两端区间后,不会出现渐变自动插值造成的边缘发虚。
想加动画,可以把 --p 注册成可动画的数值属性:
@property --p {
syntax: "<number>";
inherits: false;
initial-value: 0;
}
.ring {
transition: --p .6s ease-out;
}
这段做的是用 @property 声明 --p 的类型,浏览器才知道怎么在两个数字之间插值。不声明的话自定义属性默认按字符串处理,transition 不会生效。
需要知道的是 @property 的浏览器支持晚于 conic-gradient,Chrome 85 之后可用,Safari 16.4 之后可用。老版本上动画会退化成瞬间跳变,功能不受影响。

三、两种替代做法与取舍
3.1 border 加伪元素旋转遮罩
思路是画两个半圆,各自用 transform: rotate 转到位,再用遮罩盖掉多余部分:
.ring {
position: relative;
width: 120px;
height: 120px;
border-radius: 50%;
background: #ececec;
overflow: hidden;
}
.ring::before,
.ring::after {
content: "";
position: absolute;
width: 60px;
height: 120px;
background: #fe6a00;
}
.ring::before {
left: 0;
transform-origin: right center;
transform: rotate(calc(var(--p) * 3.6deg));
}
.ring::after {
left: 60px;
transform-origin: left center;
}
这段做的是用两个半宽矩形分别覆盖左右半圆,再靠 rotate 控制露出多少。超过 50% 的部分需要额外处理右半圆,逻辑比渐变复杂一截。
它的价值在于兼容性:不依赖渐变、不依赖 @property,很老的浏览器也能画出来。代价是进度超过一半时要分支处理,代码可读性明显下降。
3.2 SVG 描边方案
把圆画成一条有宽度的描边,用 stroke-dasharray 控制虚线长度,进度就是两个数字:
<svg viewBox="0 0 120 120" class="ring-svg">
<circle cx="60" cy="60" r="54" class="ring-track" />
<circle cx="60" cy="60" r="54" class="ring-bar" />
</svg>
.ring-svg {
width: 120px;
transform: rotate(-90deg);
}
.ring-track,
.ring-bar {
fill: none;
stroke-width: 12;
}
.ring-track {
stroke: #ececec;
}
.ring-bar {
stroke: #fe6a00;
stroke-linecap: butt;
stroke-dasharray: 339.29;
stroke-dashoffset: calc(339.29 - 339.29 * var(--p) / 100);
transition: stroke-dashoffset .6s ease-out;
}
这段做的是:先算出半径 54 的周长(2 × π × 54 ≈ 339.29),把 stroke-dasharray 设成整段周长,再用 stroke-dashoffset 把未完成的这一段推到视野外。改 --p 就改偏移,进度随之变化。
两个数字要算对:stroke-dasharray 填周长,stroke-dashoffset 填"周长减去已完成长度"。半径改了这两个数必须跟着改,否则进度会对不上。
这个方案的好处是动画天然平滑(stroke-dashoffset 是可插值属性)、跨浏览器一致、缩放不失真。代价是引入了 SVG 元素,纯 CSS 党会觉得不干净。
3.3 三种做法怎么选
| 做法 | 动画 | 兼容性 | 代码量 |
|---|---|---|---|
| conic-gradient | 需 @property | Chrome 69+ | 最少 |
| border 旋转遮罩 | 可 transition | 最老 | 中等偏多 |
| SVG 描边 | 天然平滑 | 最好 | 中等 |
真要选,按这个顺序判断:先看要不要动画,再看最低支持的浏览器版本,最后才看代码量。多数新项目选 conic-gradient 就够,需要动画直接上 SVG。
四、常见失败分支与验证清单
4.1 圆环变成椭圆
容器宽高不一致就会画成椭圆。修复办法是显式写死 width 与 height,或者用 aspect-ratio: 1 约束比例。
4.2 渐变边缘发虚
没有显式写起止角度时,渐变会在两个色标之间做插值,交界处出现模糊带。修复办法是把两个色标的起止位置写成同一个角度值。
4.3 圆心遮罩盖不住
给 .ring 补 position: relative。伪元素用 position: absolute 定位时,没有定位上下文就会往上找到更外层元素,遮罩位置直接跑偏。
4.4 验证清单
- 检查:容器 computed 面板里 width 与 height 相等
- 运行:把 --p 从 0 改到 100,观察圆环是否连续变化
- 预期:0 时全灰、100 时全橙、中间值按比例显示
- 失败:进度超过 50% 后显示错乱 → border 方案的分支逻辑没写全
- 修复:改用 conic-gradient 或 SVG 方案
- 验证:在 Chrome 与 Firefox 各看一次,两端平口一致
上面这些步骤做的是确认比例、跑通进度区间、跨浏览器比对。预期结果(未在本机执行)是两个浏览器表现一致;若只有 Firefox 异常,多半是 @property 未生效,动画退化为跳变。

总结
圆环进度条没有唯一解:静态展示用 conic-gradient 最省事,要动画和一致性选 SVG 描边,兼容旧浏览器才考虑 border 旋转遮罩。判断顺序是先看动画需求,再看浏览器版本。
三条要点带走:
- conic-gradient 加自定义属性就能驱动进度,但动画要靠 @property 声明类型;
- SVG 方案的两个数字是周长与偏移,半径一改就必须重算;
- border 遮罩方案在进度过半时需要分支,可读性最差。
下一步建议把这个圆环封装成一个接受百分比参数的组件,内部用哪种实现对外不可见,将来换方案时不用改调用方。布局细节记不清时,翻 Css Series 教程 最快。
常见问题
Q:为什么我的圆环进度条动画不生效?
A:自定义属性默认按字符串处理,浏览器不知道怎么插值。用 @property 声明 syntax 为 number,transition 才会生效。
Q:conic-gradient 画的圆环边缘发虚怎么办?
A:把两个色标的起止角度写成同一个值,让浏览器不做插值。写成两个不同角度就会出现渐变过渡带。
Q:SVG 里的 339.29 是怎么来的?
A:半径 54 的圆周长,2 × π × 54 ≈ 339.29。改半径后这个数和 stroke-dashoffset 都要重算。
Q:圆环被拉成椭圆了,怎么修?
A:容器宽高不一致导致的。写死 width 与 height,或加 aspect-ratio: 1 约束正方形。
Q:进度超过一半时显示错乱是什么原因?
A:border 旋转遮罩方案需要分别处理左右两个半圆,过半后右半圆也要参与旋转。省事的改法是换成 conic-gradient 或 SVG 方案。
延伸学习
想把圆环这几种画法练熟,可以按这个顺序来:
- 读一篇相关笔记 CSS 元素旋转效果,把 rotate 与 transform-origin 的配合看清;
- 想补更多图形技巧,Css Secrets 教程 里有成体系的实战案例;
- 想确认 SVG 元素的属性,翻 Html5 教程 里 SVG 相关章节。

TRAE-AI编程



