Let's create a Harvey Ball Progress Indicator using CSS

Posted on Jan 6, 2023

A few weeks ago, I worked on a feature for a web application. One small part of that feature was to display a progress indicator for a task. I decided to use something called Harvey Balls for this.

These are my own requirements for the solutions:

  • It should be simple.
  • It should be written purely in CSS.
  • It should be defined by CSS classes that can be applied to a single div or span element, turning it into a progress indicator.

Let’s start

As I was thinking about a solution, I came across something called conic-gradient. Let’s use it.

First, let’s define a CSS class that transforms a div / span element into a circle:

1
2
3
4
5
6
7
.progress-indicator {
    display: inline-block;
    width: 100px;
    height: 100px;
    border-radius: 50%;
    border: 2px black solid;
}

Then, we create additional classes to describe the filling of the circle in quarter steps as the progress:

 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.progress-indicator.quarter {
    background: conic-gradient(
            black 0% 25%,
            transparent 25%
    )
}

.progress-indicator.half {
    background: conic-gradient(
            black 0% 50%,
            transparent 50%
    )
}

.progress-indicator.three-quarters {
    background: conic-gradient(
            black 0% 75%,
            transparent 75%
    )
}

.progress-indicator.full {
    background: conic-gradient(
            black 0% 100%,
            transparent 100%
    )
}

Now, let’s use it in HTML:

1
2
3
4
5
<div class="progress-indicator"></div>
<div class="progress-indicator quarter"></div>
<div class="progress-indicator half"></div>
<div class="progress-indicator three-quarters"></div>
<div class="progress-indicator full"></div> 

VoilĂ , the result:

Result shows five Harvey Balls indicating a progress of 0%, 25%, 50%, 75% and 100%

Full code for this section Check it out on CodePen

Let’s improve it

But it’s possible to improve it and make it more flexible and customizable. Here’s my improved version:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.progress-indicator {
    --size: 100px;
    --color: black;
    --value: 0%;
    --border-width: 2px;
    
    display: inline-block;
    width: var(--size);
    aspect-ratio: 1 / 1;
    border-radius: 50%;
    border: var(--border-width) var(--color) solid;
    background: conic-gradient(
            var(--color) 0% var(--value),
            transparent var(--value)
    )
}

.progress-indicator.quarter {
    --value: 25%;
}

.progress-indicator.half {
    --value: 50%;
}

.progress-indicator.three-quarters {
    --value: 75%;
}

.progress-indicator.full {
    --value: 100%;
}
  1. Let’s introduce and use variables like --value, --size and --color.
  2. Since a circle has equal width and height, we can get rid of the hight by settings aspect-ratio: 1 / 1.
  3. Move background to the .progress-indicator class.
  4. Override the --value variable in the step-classes.

That’s much better, at least in my opinion

One advantage of this solution is that it’s easy to set arbitrary values:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<div class="progress-indicator"></div>
<div class="progress-indicator quarter"></div>
<div class="progress-indicator half"></div>
<div class="progress-indicator three-quarters"></div>
<div class="progress-indicator full"></div>

<!-- ADVANTAGE -->
<div class="progress-indicator" style="--value: 33%"></div>
<div class="progress-indicator" style="--value: 60%"></div>
<div class="progress-indicator" style="--value: 95%"></div>

Result shows five Harvey Balls indicating a progress of 0%, 25%, 50%, 75%, 100%, 33%, 60% and 95%

Full code for this section Check it out on CodePen

Let’s place the cherry on top

As a software engineer, I propose that we clamp the --value variable to ensure that only valid values are used in the conic-gradient. clamp(0%, var(--value), 100%) will ensure that the value used is between 0% and 100%, not below or above those limits.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
.progress-indicator {
    --size: 100px;
    --color: black;
    --value: 0%;
    --border-width: 2px;

    display: inline-block;
    width: var(--size);
    aspect-ratio: 1 / 1;
    border-radius: 50%;
    border: var(--border-width) var(--color) solid;
    background: conic-gradient(
            var(--color) 0% clamp(0%, var(--value), 100%),
            transparent clamp(0%, var(--value), 100%)
    )
}

Full code for this section Check it out on CodePen

Let’s keep it in mind for the future

CSS provides a function called attr() that can be used to retrieve the value of an attribute of the selected element and use it in the stylesheet1. However, support for properties other than content is experimental1. Keep an eye on the attr() - Browser Compatibility page for updates on this improvement.

Let’s assume that today is the day when all major browsers support attr() for all properties. We can rewrite progress-indicator as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.progress-indicator {
    --size: 100px;
    --color: black;
    --border-width: 2px;

    display: inline-block;
    width: var(--size);
    aspect-ratio: 1 / 1;
    border-radius: 50%;
    border: var(--border-width) var(--color) solid;
    background: conic-gradient(
            var(--color) 0% clamp(0%, attr(data-value, %, 0%), 100%),
            transparent clamp(0%, attr(data-value, %, 0%), 100%)
    )
}

The value can be set by a custom HTML attribute called data-value (see Using data attributes).

1
2
3
<div class="progress-indicator" data-value="33%"></div>
<div class="progress-indicator" data-value="60%"></div>
<div class="progress-indicator" data-value="95%"></div>

Full code for this section

Let’s finalize this post

Feel free to contact me anytime with feedback, questions, or just to say hello. You can find my contact channels in the footer. I’m always happy to help and love hearing from people, so don’t hesitate to reach out!


  1. You can find the original statements in the MDN Web Docs attr() documentation↩︎ ↩︎