HTML Diff
0 added 0 removed
Original 2026-02-18
Modified 2026-03-09
1 <ul><li><a>Home</a></li>
1 <ul><li><a>Home</a></li>
2 <li><a>Blog</a></li>
2 <li><a>Blog</a></li>
3 </ul><p>Jan 12, 2026</p>
3 </ul><p>Jan 12, 2026</p>
4 <p>Back in the day, building a circular progress bar meant suffering with SVG. Even now, opening DevTools on some of those old implementations still hurts. Today, things have become dramatically simpler.</p>
4 <p>Back in the day, building a circular progress bar meant suffering with SVG. Even now, opening DevTools on some of those old implementations still hurts. Today, things have become dramatically simpler.</p>
5 <p>Modern CSS lets us build a complex, flexible, and visually pleasing circular progress bar using literally a single div and a single CSS property. And all of that comes with excellent browser support.</p>
5 <p>Modern CSS lets us build a complex, flexible, and visually pleasing circular progress bar using literally a single div and a single CSS property. And all of that comes with excellent browser support.</p>
6 <p>In this article, I’ll walk through this exact technique. We’ll start with the core idea, then move on to customization options. After that, we’ll add a bit of visual “polish”, convenient control knobs, and some experimental CSS logic. Yes - at the very end, we’ll even do a bit of programming in CSS.</p>
6 <p>In this article, I’ll walk through this exact technique. We’ll start with the core idea, then move on to customization options. After that, we’ll add a bit of visual “polish”, convenient control knobs, and some experimental CSS logic. Yes - at the very end, we’ll even do a bit of programming in CSS.</p>
7 <h2>The core idea and a basic implementation</h2>
7 <h2>The core idea and a basic implementation</h2>
8 <p>Let’s start with the most important part. The entire implementation revolves around a single element:</p>
8 <p>Let’s start with the most important part. The entire implementation revolves around a single element:</p>
9 &lt;div class="progress"&gt;&lt;/div&gt;<p>And a single property - background-image with multiple background layers. The core idea is simple:</p>
9 &lt;div class="progress"&gt;&lt;/div&gt;<p>And a single property - background-image with multiple background layers. The core idea is simple:</p>
10 <ul><li><p>the top layer is a <strong>radial gradient</strong>that defines the shape of the scale;</p>
10 <ul><li><p>the top layer is a <strong>radial gradient</strong>that defines the shape of the scale;</p>
11 </li>
11 </li>
12 <li><p>the bottom layer is a <strong>conic gradient</strong>that controls the fill.</p>
12 <li><p>the bottom layer is a <strong>conic gradient</strong>that controls the fill.</p>
13 </li>
13 </li>
14 </ul><p>Here’s the basic CSS:</p>
14 </ul><p>Here’s the basic CSS:</p>
15 .progress { width: 150px; height: 150px; background-image: radial-gradient( circle, #222 0% 27%, transparent 28% 50%, #222 51% ), conic-gradient( #D64E42 0% 60%, transparent 60% ); }<p>The radial gradient on top creates the “donut” - a scale with a transparent cut-out center. The conic gradient underneath fills a sector of the circle, effectively controlling the progress.</p>
15 .progress { width: 150px; height: 150px; background-image: radial-gradient( circle, #222 0% 27%, transparent 28% 50%, #222 51% ), conic-gradient( #D64E42 0% 60%, transparent 60% ); }<p>The radial gradient on top creates the “donut” - a scale with a transparent cut-out center. The conic gradient underneath fills a sector of the circle, effectively controlling the progress.</p>
16 <p>It’s worth noting that this approach relies on old - one might even say “ancient” - CSS features: multiple backgrounds, radial and conic gradients, and hard color transitions.</p>
16 <p>It’s worth noting that this approach relies on old - one might even say “ancient” - CSS features: multiple backgrounds, radial and conic gradients, and hard color transitions.</p>
17 <p>Basic circular progress bar implementation</p>
17 <p>Basic circular progress bar implementation</p>
18 <p>This is the key technique that forms the foundation of the progress bar. Everything that follows is just customization and layering on top of it.</p>
18 <p>This is the key technique that forms the foundation of the progress bar. Everything that follows is just customization and layering on top of it.</p>
19 <h2>Customization: scale thickness and transparency</h2>
19 <h2>Customization: scale thickness and transparency</h2>
20 <p>One of the nicest things about this approach is how easy it is to customize.</p>
20 <p>One of the nicest things about this approach is how easy it is to customize.</p>
21 <p><strong>Scale thickness</strong>is controlled by the parameters of the top, radial gradient. We simply tweak the color stop positions:</p>
21 <p><strong>Scale thickness</strong>is controlled by the parameters of the top, radial gradient. We simply tweak the color stop positions:</p>
22 radial-gradient( circle, #222 0% 30%, transparent 31% 54%, #222 55% )<p><strong>Color and transparency of both the scale and the fill</strong>are controlled by the bottom, conic gradient. Change the first color stop to a semi-transparent color - and the fill becomes translucent. Change the second color stop - and the empty part of the scale changes.</p>
22 radial-gradient( circle, #222 0% 30%, transparent 31% 54%, #222 55% )<p><strong>Color and transparency of both the scale and the fill</strong>are controlled by the bottom, conic gradient. Change the first color stop to a semi-transparent color - and the fill becomes translucent. Change the second color stop - and the empty part of the scale changes.</p>
23 conic-gradient( rgba(214, 78, 66, 0.6) 0% 60%, transparent 60% )<p>No extra elements, no masks, no calculations. Just gradient parameters.</p>
23 conic-gradient( rgba(214, 78, 66, 0.6) 0% 60%, transparent 60% )<p>No extra elements, no masks, no calculations. Just gradient parameters.</p>
24 <p>Adjusting scale thickness and fill transparency</p>
24 <p>Adjusting scale thickness and fill transparency</p>
25 <h2>Making it look good: texture, borders, and shadows</h2>
25 <h2>Making it look good: texture, borders, and shadows</h2>
26 <p>The basic version works, but it looks a bit rough. Let’s add some visual “expensiveness”.</p>
26 <p>The basic version works, but it looks a bit rough. Let’s add some visual “expensiveness”.</p>
27 <p>First, we add a third background layer - a repeating conic gradient that creates subtle radial sections:</p>
27 <p>First, we add a third background layer - a repeating conic gradient that creates subtle radial sections:</p>
28 repeating-conic-gradient( rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.2) 4.5%, transparent 5% )<p>Second, we make the top radial gradient more complex by adding intermediate colors to create thin borders:</p>
28 repeating-conic-gradient( rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.2) 4.5%, transparent 5% )<p>Second, we make the top radial gradient more complex by adding intermediate colors to create thin borders:</p>
29 radial-gradient( circle, #222 0% 27%, #333, transparent 28% 50%, #333, #222 51% )<p>And finally, we slightly round the progress bar itself and add shadows:</p>
29 radial-gradient( circle, #222 0% 27%, #333, transparent 28% 50%, #333, #222 51% )<p>And finally, we slightly round the progress bar itself and add shadows:</p>
30 .progress { border-radius: 10px; box-shadow: inset 0 0 1px #666, 0 0 30px black; }<p>None of this affects the logic of the component - it’s purely visual.</p>
30 .progress { border-radius: 10px; box-shadow: inset 0 0 1px #666, 0 0 30px black; }<p>None of this affects the logic of the component - it’s purely visual.</p>
31 <p>Final styling</p>
31 <p>Final styling</p>
32 <h2>Control knobs: CSS variables</h2>
32 <h2>Control knobs: CSS variables</h2>
33 <p>Now let’s make the progress bar practical for real interfaces. To do that, we introduce a CSS variable called --progress.</p>
33 <p>Now let’s make the progress bar practical for real interfaces. To do that, we introduce a CSS variable called --progress.</p>
34 .progress { --progress: 45; --corner: calc(1% * var(--progress, 0)); background-image: radial-gradient( circle, #222 0% 27%, #333, transparent 28% 50%, #333, #222 51% ), conic-gradient( #D64E42 0% var(--corner), transparent var(--corner) ); }<p>Now progress is expressed as a clear number from 0 to 100, and CSS automatically converts it into the required angle.</p>
34 .progress { --progress: 45; --corner: calc(1% * var(--progress, 0)); background-image: radial-gradient( circle, #222 0% 27%, #333, transparent 28% 50%, #333, #222 51% ), conic-gradient( #D64E42 0% var(--corner), transparent var(--corner) ); }<p>Now progress is expressed as a clear number from 0 to 100, and CSS automatically converts it into the required angle.</p>
35 <p>It’s easy to create multiple instances of the component:</p>
35 <p>It’s easy to create multiple instances of the component:</p>
36 &lt;div class="progress" style="--progress: 15"&gt;&lt;/div&gt; &lt;div class="progress" style="--progress: 50"&gt;&lt;/div&gt; &lt;div class="progress" style="--progress: 90"&gt;&lt;/div&gt;<p>Controlling progress with a CSS variable</p>
36 &lt;div class="progress" style="--progress: 15"&gt;&lt;/div&gt; &lt;div class="progress" style="--progress: 50"&gt;&lt;/div&gt; &lt;div class="progress" style="--progress: 90"&gt;&lt;/div&gt;<p>Controlling progress with a CSS variable</p>
37 <h2>Experimental CSS features: conditional logic</h2>
37 <h2>Experimental CSS features: conditional logic</h2>
38 <p>Now let’s take a small peek into the future of CSS. In modern versions of Chrome, you can already use conditional logic via if().</p>
38 <p>Now let’s take a small peek into the future of CSS. In modern versions of Chrome, you can already use conditional logic via if().</p>
39 <p>For example, you can change the scale color depending on the progress value:</p>
39 <p>For example, you can change the scale color depending on the progress value:</p>
40 .progress { --color: if( style(--progress &lt;= 33): #D64E42; style(--progress &lt;= 66): #F5B848; style(--progress &lt;= 99): #58B473; else: white; ); }<p>And then use that color in the gradient:</p>
40 .progress { --color: if( style(--progress &lt;= 33): #D64E42; style(--progress &lt;= 66): #F5B848; style(--progress &lt;= 99): #58B473; else: white; ); }<p>And then use that color in the gradient:</p>
41 conic-gradient( var(--color) 0% var(--corner), transparent var(--corner) )<p>You can also add a label in the center of the progress bar that changes depending on the progress value. We add a pseudo-element and use if() in the content property to bind the label text to progress ranges. For the label color, we reuse the parent’s --color variable.</p>
41 conic-gradient( var(--color) 0% var(--corner), transparent var(--corner) )<p>You can also add a label in the center of the progress bar that changes depending on the progress value. We add a pseudo-element and use if() in the content property to bind the label text to progress ranges. For the label color, we reuse the parent’s --color variable.</p>
42 .progress { display: flex; justify-content: center; align-items: center; } .progress::before { content: if( style(--progress &lt;= 33): "low"; style(--progress &lt;= 66): "med"; style(--progress &lt;= 99): "high"; else: "go!"; ); color: var(--color); }<p>Important: these are experimental features. If you need solid browser support, use JavaScript together with modifier classes instead.</p>
42 .progress { display: flex; justify-content: center; align-items: center; } .progress::before { content: if( style(--progress &lt;= 33): "low"; style(--progress &lt;= 66): "med"; style(--progress &lt;= 99): "high"; else: "go!"; ); color: var(--color); }<p>Important: these are experimental features. If you need solid browser support, use JavaScript together with modifier classes instead.</p>
43 <p>Conditional styling based on progress</p>
43 <p>Conditional styling based on progress</p>
44 <h2>Wow factor: native animation</h2>
44 <h2>Wow factor: native animation</h2>
45 <p>And finally - the fun part. This progress bar works beautifully with native CSS animations.</p>
45 <p>And finally - the fun part. This progress bar works beautifully with native CSS animations.</p>
46 <p>First, we register the --progress variable:</p>
46 <p>First, we register the --progress variable:</p>
47 @property --progress { inherits: true; initial-value: 0; syntax: "&lt;number&gt;"; }<p>And then animate it:</p>
47 @property --progress { inherits: true; initial-value: 0; syntax: "&lt;number&gt;"; }<p>And then animate it:</p>
48 .progress { animation: progress 10s infinite alternate; } @keyframes progress { to { --progress: 100; } }<p>During the animation, the following update automatically:</p>
48 .progress { animation: progress 10s infinite alternate; } @keyframes progress { to { --progress: 100; } }<p>During the animation, the following update automatically:</p>
49 <ul><li><p>the fill size,</p>
49 <ul><li><p>the fill size,</p>
50 </li>
50 </li>
51 <li><p>the scale color,</p>
51 <li><p>the scale color,</p>
52 </li>
52 </li>
53 <li><p>the center label.</p>
53 <li><p>the center label.</p>
54 </li>
54 </li>
55 </ul><p>In other words, all styles tied to --progress are recalculated on the fly as the native animation updates the variable.</p>
55 </ul><p>In other words, all styles tied to --progress are recalculated on the fly as the native animation updates the variable.</p>
56 <p>Progress bar animation</p>
56 <p>Progress bar animation</p>
57 <h2>Conclusion</h2>
57 <h2>Conclusion</h2>
58 <p>The key technique throughout this article is using multiple backgrounds with radial and conic gradients. It’s simple, well-supported, and extremely flexible.</p>
58 <p>The key technique throughout this article is using multiple backgrounds with radial and conic gradients. It’s simple, well-supported, and extremely flexible.</p>
59 <p>Everything else - customization, visual effects, conditional logic, animation - is just an optional layer on top.</p>
59 <p>Everything else - customization, visual effects, conditional logic, animation - is just an optional layer on top.</p>
60 <p>If you need a reliable production-ready component, stick to the basic technique. If you’re in an experimental mood and browser support allows it, try the newer features - they’re already available in recent versions of Chrome.</p>
60 <p>If you need a reliable production-ready component, stick to the basic technique. If you’re in an experimental mood and browser support allows it, try the newer features - they’re already available in recent versions of Chrome.</p>
61 <p>The complete code lives in an <a>interactive, step-by-step demo</a> - you can go through the implementation at your own pace and play with the parameters along the way.</p>
61 <p>The complete code lives in an <a>interactive, step-by-step demo</a> - you can go through the implementation at your own pace and play with the parameters along the way.</p>
62 <p>Hopefully, building progress bars in 2026 will feel a lot more enjoyable.</p>
62 <p>Hopefully, building progress bars in 2026 will feel a lot more enjoyable.</p>