旋转进度条通常用于展示任务完成度或加载状态,它比线性进度条更节省横向空间,也更容易融入卡片、仪表盘和移动端界面。实现一个纯CSS旋转进度条,核心思路是让颜色沿着圆周按比例填充,同时保留中间空白区域形成圆环。传统做法会使用两个半圆容器配合rotate旋转,现代浏览器则可以直接使用conic-gradient完成同样的效果。本文从易于维护和动态更新的角度,优先介绍conic-gradient方案,再对比半圆方案,帮助理解旋转和角度计算。

一、用conic-gradient画出基础圆环
conic-gradient是CSS中专门用于圆锥渐变的函数,它的渐变起点默认在元素顶部,然后按顺时针方向绕中心旋转,非常适合制作圆形进度。想让进度条按百分比显示,只需要把前一部分设置为主题色,剩余部分设置为浅灰色。示例中calc(var(--progress) * 1%)用来把0到100的数值转换成0%到100%的百分比,#4f8cff从顶部开始填充到该百分比位置,紧接着#e8ecf1覆盖剩余部分。
为了把实心圆变成圆环,最直接的方式是在内部叠加一个与背景色相同的圆形。这里使用::after伪元素,通过inset控制内圆与外层边框的距离,该距离就是圆环的粗细。伪元素设置border-radius:50%后,会覆盖中心区域,只露出外圈的渐变部分。进度文本需要放在伪元素上方,因此可以给文本容器设置相对定位并提高层级。
<div class="progress-ring" style="--progress: 72;"> <span class="progress-text">72%</span> </div>
.progress-ring {
--progress: 0;
position: relative;
width: 150px;
height: 150px;
border-radius: 50%;
background: conic-gradient(
#4f8cff calc(var(--progress) * 1%),
#e8ecf1 0
);
display: flex;
align-items: center;
justify-content: center;
}
.progress-ring::after {
content: "";
position: absolute;
inset: 14px;
border-radius: 50%;
background: #fff;
}
.progress-text {
position: relative;
z-index: 1;
font-size: 28px;
font-weight: 600;
color: #333;
}
这段结构非常简单,外层.progress-ring负责渐变圆盘和尺寸,伪元素负责挖空中心,.progress-text显示百分比。当--progress变化时,calc会重新计算颜色边界,进度条就会按照新数值渲染。需要注意的是,如果内圆背景色与页面背景不一致,可以将其设置为与容器所在背景相同的颜色,或者改用mask方案彻底透明。
二、用CSS变量驱动进度更新与过渡
静态圆环只是第一步,实际业务中进度值通常来自上传进度、接口返回或计时器。CSS变量非常适合承担这个桥梁角色。通过style.setProperty修改行内样式中的--progress,无需改动样式表,就能让圆环实时响应。下面是一段简单的JavaScript更新逻辑,它每50毫秒增加1%,直到100%。
const ring = document.querySelector('.progress-ring');
const text = ring.querySelector('.progress-text');
let value = 0;
const timer = setInterval(() => {
value = Math.min(value + 1, 100);
ring.style.setProperty('--progress', value);
text.textContent = value + '%';
if (value >= 100) {
clearInterval(timer);
}
}, 50);
如果希望进度变化时颜色边界平滑移动,需要给自定义属性注册类型。使用@property声明--progress为数值类型后,再给.progress-ring设置transition: --progress 0.25s ease。这样每次数值更新都会被浏览器插值,颜色边界会平滑旋转到新位置,而不是瞬间跳变。需要注意的是,@property目前在现代浏览器中支持良好,但在较旧环境中不会生效,此时可以退化为直接跳变,不影响功能。
@property --progress {
syntax: '<number>';
initial-value: 0;
inherits: false;
}
.progress-ring {
transition: --progress 0.25s ease;
}
这种方案的维护成本很低,进度值只需要一个数字,圆环厚度、颜色、尺寸都集中在CSS中。如果要加环形阴影、渐变高光,也只需要调整background和伪元素样式,不会影响核心逻辑。
三、半圆容器方案与遮罩处理
在conic-gradient普及之前,开发者通常使用两个半圆容器加overflow隐藏的方式来实现圆形进度。其思路是把一个完整圆分成左右两半,每半容器内放置一个宽度翻倍的填充层,通过旋转填充层让颜色逐渐露出。右半容器负责0%到50%的进度,超过50%后左半容器开始参与旋转。这种方案的优势是兼容更老的浏览器,但代码量和状态判断明显增加。
半圆方案的HTML结构会多出几个嵌套层,基本形态如下。外层容器是圆形,内部左右两个容器各占一半宽度,并且都设置overflow:hidden,保证只显示所属半区的内容。填充层宽度需要是容器宽度的两倍,这样才能形成完整圆盘的一半。旋转原点要放在半圆的圆心,也就是左半容器内的右边界中点,或右半容器内的左边界中点。
<div class="legacy-ring">
<div class="legacy-left">
<div class="legacy-left-fill"></div>
</div>
<div class="legacy-right">
<div class="legacy-right-fill"></div>
</div>
</div>
.legacy-ring {
position: relative;
width: 140px;
height: 140px;
}
.legacy-left,
.legacy-right {
position: absolute;
top: 0;
width: 50%;
height: 100%;
overflow: hidden;
}
.legacy-left {
left: 0;
}
.legacy-right {
right: 0;
}
.legacy-left-fill,
.legacy-right-fill {
position: absolute;
width: 200%;
height: 100%;
background: #4f8cff;
border-radius: 50%;
}
.legacy-left-fill {
left: 0;
transform-origin: right center;
}
.legacy-right-fill {
right: 0;
transform-origin: left center;
}
这个方法的关键在于旋转逻辑:在50%以下时,只旋转右半填充层,左半保持隐藏;超过50%后,右半填充层固定旋转到180度,同时开始旋转左半填充层。由于填充层是实心圆盘,默认会形成饼图进度,如果要得到圆环,还需要再叠加一个中心圆形遮罩。可以看出,conic-gradient方案减少了大量状态判断,更推荐在新项目中使用。
四、旋转加载动画与细节优化
有时进度条不是为了展示实际百分比,而是表示正在加载。这种情况下可以给圆环添加一个连续旋转动画。做法是让整个.progress-ring持续旋转,同时保持内部文本不转或反向旋转。对于纯加载指示器,通常不需要中心文本,直接使用一个半透明圆环加旋转动画即可。
.loading-ring {
width: 60px;
height: 60px;
border: 6px solid #e8ecf1;
border-top-color: #4f8cff;
border-radius: 50%;
animation: spin 0.9s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
这个简化版加载环只使用四边边框,把顶部边框设置为主题色,其他边框保持浅灰,再配合animation让整个元素旋转。与前面基于百分比的圆环不同,它重点强调系统正在工作,不展示精确进度。这种旋转动画可以和conic-gradient方案组合,例如在进度达到100%前显示一个外圈旋转高光,但要注意动画层和进度层不要互相覆盖。
在实际项目中还需要考虑无障碍和可读性。承载进度信息的元素建议设置role="progressbar"和aria-valuenow,颜色对比度也要满足基本要求。对于移动端,尺寸不宜太小,圆环厚度至少4像素,否则在小屏幕上难以辨认。以上方案全部基于纯CSS和少量JavaScript,没有额外请求图片,适合作为组件嵌入到各类前端框架中。