Build a Customizable Progress Indicator with Pure CSS: Harvey Ball Example

Posted on

Introduction

While developing a recent web application, I needed an intuitive and visual way to display task progress. After considering various options, I chose Harvey Balls: circular progress indicators that are commonly used in charts and reports. In this tutorial, I’ll walk you through how to create Harvey Balls with pure CSS, making them flexible and customizable for any use case.

We’ll use only CSS, with no external libraries, to create a progress indicator that’s not only easy to implement but also highly adaptable for different design needs.

Requirements

Here’s what we’re aiming for:

  • A simple, clean solution using pure CSS.
  • Customizable indicators that can be easily applied to any div or span element.
  • Flexibility to adjust the size, color, and progress value with minimal effort.

Step 1: Basic Harvey Ball Setup

To start, we’ll transform a div or span element into a circular progress indicator.

1
2
3
4
5
6
7
.progress-indicator {
    display: inline-block;
    width: 100px;
    height: 100px;
    border-radius: 50%;      /* Makes the element circular */
    border: 2px black solid; /* Adds a border */
}

Apply this class in your HTML:

1
<div class="progress-indicator"></div>

Result shows an empty Harvey Balls

Check out the code on GitHub Try it out on CodePen

Step 2: Creating Progress States

Now, let’s use conic-gradient function, which is ideal for creating circular progress bars, to define the different progress states: quarter, half, three-quarters, and full.

 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%,           /* Black color for the first 25% */
            transparent 25%         /* The rest remains transparent */
    )
}

.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%
    )
}

In your HTML, you can now apply these classes to visualize progress:

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> 

Here’s what the result looks like:

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

Check out the code on GitHub Try it out on CodePen

Step 3: Improving Customization with CSS Variables

To make the Harvey Ball more flexible, we can introduce CSS variables for the size, color, and progress value. This allows you to easily adjust these properties for different use cases.

 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
33
.progress-indicator {
    --size: 100px;              /* Size of the circle */
    --color: black;             /* Color of the progress fill */
    --value: 0%;                /* Percentage of the progress */
    --border-width: 2px;        /* Border thickness */
    
    display: inline-block;
    width: var(--size);         /* Set the width based on the variable */
    aspect-ratio: 1 / 1;        /* Ensures the element is a perfect circle */
    border-radius: 50%;
    border: var(--border-width) var(--color) solid;
    background: conic-gradient(
            var(--color) 0% var(--value),   /* Fill progress */
            transparent var(--value)        /* Transparent for the rest */
    )
}

/* Override the --value variable to define progress */
.progress-indicator.quarter {
    --value: 25%;
}

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

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

.progress-indicator.full {
    --value: 100%;
}

This approach makes it easy to create progress indicators with custom values by overriding the --value variable either in the CSS class or directly within the style attribute. For example:

 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 eight Harvey Balls indicating a progress of 0%, 25%, 50%, 75%, 100%, 33%, 60% and 95%

Check out the code on GitHub Try it out on CodePen

Step 4: Adding Value Clamping for Extra Safety

To ensure the --value variable stays within the 0% – 100% range, we can use the clamp() function. This guarantees that any invalid values, such as those below 0% or above 100%, are automatically corrected to fit within the valid range.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
.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(
        /* Clamps the progress value between 0% and 100% */
        var(--color) 0% clamp(0%, var(--value), 100%),
        transparent clamp(0%, var(--value), 100%)
    )
}

Example in HTML:

1
2
<div class="progress-indicator" style="--value: -50%"></div>
<div class="progress-indicator" style="--value: 150%"></div> 

Result shows two Harvey Balls indicating a progress of 0% and 100%

Check out the code on GitHub Try it out on CodePen

The first div has an invalid value of -50%, but thanks to the clamp() function, it is corrected to 0%, showing no progress. Similarly, the second div is set to 150%, but clamp() ensures the value does not exceed 100%, resulting in a fully filled Harvey Ball.

Bonus: Future-Proofing with Experimental CSS attr()

Looking ahead, CSS might soon support the attr() function for properties other than content. If browser support for attr() expands, we could make the progress indicator even more dynamic by pulling the value directly from a custom HTML attribute.

Future Compatibility Only
The technique described in this section using the attr() function is currently experimental and not fully supported by all browsers. At the time of writing, attr() can only be used for the content property in CSS, but future support for other properties is being explored. Be sure to check browser compatibility before using this method in production environments.

Here’s an example of what that might look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
.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(
        /* Uses data-value from the HTML */
        var(--color) 0% clamp(0%, attr(data-value, %, 0%), 100%),
        transparent clamp(0%, attr(data-value, %, 0%), 100%)
    )
}

Then, set the progress value directly in your HTML using the data attribute:

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>

Check out the code on GitHub

Conclusion

Creating a Harvey Ball progress indicator using pure CSS is not only easy but also incredibly useful for displaying progress in a clean, minimalist way. With the flexibility of CSS variables, you can easily customize the size, color, and percentage of completion.

Feel free to experiment with your own custom progress indicators! If you have any questions or feedback, don’t hesitate to reach out via my contact information in the footer.

Check out the code on GitHub Try it out on CodePen