` are the two **containers** we were talking about in the previous section. Their purpose is to use the clip css property to hide and show part of their children elements.
Now let's apply some basic css to get everything in place:
index.css
```css
/* this css rule is used only to help us with visual debug */
* {
border: 1px solid red;
}
body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.container {
width: 80%;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container input {
width: 100%;
margin: 0px 30px;
}
.circular {
position: relative;
margin: 0;
padding: 0;
/*
* Diameter of the circle: 500px,
* we'll fix the fact that this value is hardcoded later
*/
height: 500px;
width: 500px;
}
.circular .bar {
position: absolute;
height: 100%;
width: 100%;
border-radius: 50%;
}
.circle .bar .progress {
position: absolute;
height: 100%;
width: 100%;
border-radius: 50%;
}
```
All this css is pretty straight forward, the only things to note are:
- `* { border: 1px solid red; }` is used only for visual debug purpose
- in the `.circular { /*...*/ }` rule we hardcode the height and the width to match the diamater of the circle of the progress bar we are going to create, but this is only temporary, we'll remove it later.
All that is left is some basic JavaScript:
index.js
```js
/**
* Waits for the document to be ready before running the init function
*/
function docReady(fn) {
if (document.readyState === "complete"
|| document.readyState === "interactive") {
setTimeout(fn, 1);
} else {
document.addEventListener("DOMContentLoaded", fn);
}
}
docReady(init);
function init() {
// set up will happen here...
makeProgressBarInteractive();
}
function makeProgressBarInteractive() {
let inputRef = document.getElementsByTagName("input")[0];
inputRef.addEventListener("input", updateCiruclarProgressBar);
}
function updateCiruclarProgressBar(event) {
console.log(event.target.value);
// fix the rotation of the circles to match the slider value...
}
```
### Step 2: Clip
The next step is to define a **diameter** in pixels and a **color** for our circles:
```js
const DIAMETER = 500;
const COLOR = "#ff0000";
```
Now the `init` function will get the references of all the `
` elements and set up the **background-color** and the **clip** properties:
```js
let inputRef = null;
let rightBar = null;
let leftBar = null;
let rightProgress = null;
let leftProgress = null;
function init() {
rightBar = document.getElementsByClassName("bar")[0];
leftBar = document.getElementsByClassName("bar")[1];
rightProgress = document.getElementsByClassName("progress")[0];
leftProgress = document.getElementsByClassName("progress")[1];
// clip the right part of the circle
rightBar.style.clip = `rect(0px, ${DIAMETER}px, ${DIAMETER}px, ${DIAMETER / 2}px)`;
// clip the left part of the circle
rightProgress.style.clip = `rect(0px, ${DIAMETER / 2}px, ${DIAMETER}px, 0px)`;
// clip the left part of the cicle
leftBar.style.clip = `rect(0px, ${DIAMETER / 2}px, ${DIAMETER}px, 0px)`;
// clip the right part of the circle
leftProgress.style.clip = `rect(0px, ${DIAMETER}px, ${DIAMETER}px, ${DIAMETER / 2}px)`;
// set background color
rightProgress.style.backgroundColor = `${COLOR}`;
leftProgress.style.backgroundColor = `${COLOR}`;
makeProgressBarInteractive();
}
```

### Step 3: Rotate
Now the fun part! Let's rotate the `rightProgress` and `leftProgress` elements accordingly to the percentage value of the slider.

```js
function makeProgressBarInteractive() {
inputRef = document.getElementsByTagName("input")[0];
inputRef.addEventListener("input", updateCiruclarProgressBar);
}
function updateCiruclarProgressBar(event) {
// percentage value is get from the slider
const percentage = event.target.value / 100;
// the right side of the circle handles the progress from 0% up to 50%
// if the progress is over 50% then the rotation will max at 180 degrees
rightProgress.style.transform = `rotate(${percentage < 0.5 ? percentage * 2 * 180 : 180}deg)`;
// the left side of the circle handles the progress from 50% up to 100%
// if the progress is under 50% then the rotation will be of 0 degrees.
leftProgress.style.transform = `rotate(${percentage > 0.5 ? percentage * 2 * 180 + 180 : 0}deg)`;
}
```
An important thing to note is that the `leftProgress` element must start to rotate only when the percentage of progress reaches 50%, on the other side, the `rightProgress` element must stop his rotation when the percentage of progress reaches 50%.
### Step 4: Polishing the details
To have a beautiful circle all that is left is to get rid of the `* { border: 1px solid red; }` property:

We also need to handle with JavaScript the question of the height and width that was initially hardcoded in the css:
```css
.circular {
/*...*/
/* remove -> height: 500px; */
/* remove -> width: 500px; */
}
```
and replace it with:
```js
let circularRef = null;
function init() {
circularRef = document.getElementsByClassName("circular")[0];
circularRef.style.height = DIAMETER + "px";
circularRef.style.width = DIAMETER + "px";
//...
}
```
At the end our JavaScript file will look like this:
```js
const DIAMETER = 500;
const COLOR = "#ff0000";
let inputRef = null;
let rightBar = null;
let leftBar = null;
let rightProgress = null;
let leftProgress = null;
let circularRef = null;
function makeProgressBarInteractive() {
inputRef = document.getElementsByTagName("input")[0];
inputRef.addEventListener("input", updateCiruclarProgressBar);
inputRef.value = 0;
}
function init() {
circularRef = document.getElementsByClassName("circular")[0];
circularRef.style.height = DIAMETER + "px";
circularRef.style.width = DIAMETER + "px";
rightBar = document.getElementsByClassName("bar")[0];
leftBar = document.getElementsByClassName("bar")[1];
rightProgress = document.getElementsByClassName("progress")[0];
leftProgress = document.getElementsByClassName("progress")[1];
rightBar.style.clip = `rect(0px, ${DIAMETER}px, ${DIAMETER}px, ${DIAMETER / 2}px)`;
rightProgress.style.clip = `rect(0px, ${DIAMETER / 2}px, ${DIAMETER}px, 0px)`;
leftBar.style.clip = `rect(0px, ${DIAMETER / 2}px, ${DIAMETER}px, 0px)`;
leftProgress.style.clip = `rect(0px, ${DIAMETER}px, ${DIAMETER}px, ${DIAMETER / 2}px)`;
rightProgress.style.backgroundColor = `${COLOR}`;
leftProgress.style.backgroundColor = `${COLOR}`;
makeProgressBarInteractive();
}
function updateCiruclarProgressBar(event) {
const percentage = event.target.value / 100;
rightProgress.style.transform = `rotate(${percentage < 0.5 ? percentage * 2 * 180 : 180}deg)`;
leftProgress.style.transform = `rotate(${percentage > 0.5 ? percentage * 2 * 180 + 180 : 0}deg)`;
}
function docReady(fn) {
if (document.readyState === "complete" || document.readyState === "interactive") {
setTimeout(fn, 1);
} else {
document.addEventListener("DOMContentLoaded", fn);
}
}
docReady(init);
```
### Step 5: Content inside the circle

Now that we have a full circle that fills up based on a percentage, the next step would be to put some content inside the circle. I have decided to simply show a slightly smaller circle colored as the background (white) with the percentage written in the center.
index.html
```html
```
index.css
```css
/* ... */
.content {
position: absolute;
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
}
```
index.js
```js
const BORDER_WIDTH = 20;
const BACKGROUND_COLOR = "#ffffff";
let content = null;
// ...
function init() {
// ...
content = document.getElementsByClassName("content")[0];
content.style.height = DIAMETER - BORDER_WIDTH + "px";
content.style.width = DIAMETER - BORDER_WIDTH + "px";
content.style.backgroundColor = BACKGROUND_COLOR;
// ...
}
function updateCiruclarProgressBar(event) {
// ...
content.firstElementChild.innerHTML = `${event.target.value}%`;
}
```
The final result looks like this:

## 4. Let's make it in React
Now that we have a good understangind of the mechanics behind this circular progress bar, let's wrap everything we have said so far in a **React component**.
We want the react component to be **as customizable as possible** throught props like:
- **percentage of progress**
- **diameter**
- **color**
- **border width**
- **content background color**
- **content as a children element**

**Note**: this component is **typescript friendly**.
circularProgressBar.tsx
```ts
import { MutableRefObject, useEffect, useRef } from "react";
import "./circularProgressBar.css";
interface circularProgressBarPropsInterface {
color: string;
diameter: number;
percentage: number;
borderWidth?: number;
contentBackgroundColor?: string;
className?: string;
contentClassName?: string;
children?: JSX.Element;
}
export default function CircularProgressBar({
color,
diameter,
percentage,
borderWidth,
contentBackgroundColor,
className,
contentClassName,
children,
}: circularProgressBarPropsInterface) {
const rightBar = useRef(null) as MutableRefObject;
const rightProgress = useRef(null) as MutableRefObject;
const leftBar = useRef(null) as MutableRefObject;
const leftProgress = useRef(null) as MutableRefObject;
const content = useRef(null) as MutableRefObject;
// setup dimensions, colors, and clip properties
useEffect(() => {
if (borderWidth) {
content.current.style.height = diameter - borderWidth + "px";
content.current.style.width = diameter - borderWidth + "px";
}
if (contentBackgroundColor) {
content.current.style.backgroundColor = contentBackgroundColor;
}
rightBar.current.style.clip = `rect(0px, ${diameter}px, ${diameter}px, ${diameter / 2}px)`;
rightProgress.current.style.clip = `rect(0px, ${diameter / 2}px, ${diameter}px, 0px)`;
leftBar.current.style.clip = `rect(0px, ${diameter / 2}px, ${diameter}px, 0px)`;
leftProgress.current.style.clip = `rect(0px, ${diameter}px, ${diameter}px, ${diameter / 2}px)`;
rightProgress.current.style.backgroundColor = `${color}`;
leftProgress.current.style.backgroundColor = `${color}`;
}, [color, diameter, borderWidth, contentBackgroundColor]);
// handles the rotation
useEffect(() => {
if (!percentage) {
rightProgress.current.style.transform = `rotate(0deg)`;
leftProgress.current.style.transform = `rotate(0deg)`;
return;
}
let floatPercentage = percentage / 100;
rightProgress.current.style.transform = `rotate(${floatPercentage < 0.5 ? floatPercentage * 2 * 180 : 180}deg)`;
leftProgress.current.style.transform = `rotate(${floatPercentage > 0.5 ? floatPercentage * 2 * 180 + 180 : 0}deg)`;
}, [percentage]);
return (
);
}
```
circularProgressBar.css
```css
.container {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.circular {
position: relative;
margin: 0;
padding: 0;
}
.circular .bar {
position: absolute;
height: 100%;
width: 100%;
border-radius: 50%;
}
.circle .bar .progress {
position: absolute;
height: 100%;
width: 100%;
border-radius: 50%;
}
.content {
position: absolute;
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
}
```
That's it. Now we have a fully customized and reusable circular progress bar that we can easily throw in any of our projects.
## 3. npm package
If you want to use this circular progress bar in your React application we have made an **[npm package](https://link.to.thingy)** for you.
First we need to install the package with **npm**:
```
npm install @monade/react-circular-progress-bar
```
or with **yarn**:
```
yarn add @monade/react-circular-progress-bar
```
Then we just need to **import** and **use** it like this:
```js
import { CircularProgressBar } from "@monade/react-circular-progress-bar";
```
```html
{...}
```
## 4. Use case: Hippopod's Player
My journey through the problem of circular progress bars in web development started when I was working on **Hippopod**, an open source project that takes an RSS feed and transform it in a static podcast websites.
During the development I found myself having to develop an audio player, whose play / pause button component had to take care of showing the progress of the track being played like this:

If you are interested in what Hippopod is, check it out at **[hippopod.xyz](https://hippopod.xyz/)**