Written by Uzochukwu Eddie Odozi✏️
Progress bars are used to indicate activities like file uploads and downloads, page loading, user counts, and more on desktop or mobile devices. This visual representation can go a long way toward enhancing the user experience of your app.
In this tutorial, we’ll demonstrate how to create a simple, customizable, easy-to-use circular progress bar component from Scalable Vector Graphics (SVGs) using React. We’ll do so using no external dependencies.
Here’s what the circular progress component will look like:
You can reference the full source code for this tutorial in the GitHub repo.
Let’s dive in!
Getting started
Before we start, we must first create a React application. We’ll use create-react-app
with npx to create our app. I’ll assume you have Node.js installed on your computer.
Open a terminal or command prompt, navigate to the directory where you want to add your project and type the following command.
npx create-react-app react-progress-bar
You can open the project with any IDE of your choice.
create-react-app
creates a src
directory. This is the directory that contains the entry component (App.js
) of our application and where other components will be created. Delete the contents of the index.css
file and add:
body {
margin: 0;
}
In the App.css
file, delete all CSS styles except for the classes App
and App-header
. You can change both class names to be lowercase. Inside the App.js
component file, delete the contents of the header element and change it to a div.
<div className="app">
<div className="app-header">
</div>
</div>
create-react-app
creates the component inside App.js
as a functional component. You can use the default definition of the function or change it to an arrow function.
Progress component setup
To create a progress component, create a folder called progress
and add two files ProgressBar.js
and ProgressBar.css
. Inside the ProgressBar.js
file, create an arrow function ProgressBar
and export function as default. Set the parent element to Fragment
(import from React) or empty tags.
Basic SVG
Scalable Vector Graphics (SCGs) are used to define vector-based graphics for the web, according to W3 Schools.
The first element to add to the progress bar component is the <svg>
element tag, which defines a container for a coordinate system and viewport.
import React from 'react';
import './ProgressBar.css';
const ProgressBar = () => {
return (
<>
<svg>
</svg>
</>
);
}
export default ProgressBar;
The svg
element can accept numerous attributes; we’ll add width
and height
. The width and height of the SVG container will be dynamic, so we’ll add both as props.
return (
<>
<svg className="svg" width={} height={}>
</svg>
</>
);
Inside the added <svg>
element, place a <circle>
tag to create a circle. In the <circle>
element, declare the radius r
of the circle and the x-coordinate (cx
) and y-coordinate (cy
) of its center.
Additionally, we’ll define the stroke (color) and stroke width of the circle. I’ll define two separate <circle>
elements:
<svg className="svg" width={} height={}>
<circle
className="svg-circle-bg"
stroke={}
cx={}
cy={}
r={}
strokeWidth={}
/>
<circle
className="svg-circle"
stroke={}
cx={}
cy={}
r={}
strokeWidth={}
/>
</svg>
The first circle element displays the inner circle while the second is placed on top of the first element to display the progress color based on the percentage calculated.
Next, add a <text></text>
element, which draws a graphics element consisting of text. We’ll also add the attributes x
and y
, which represent the x and y starting points of the text.
<svg className="svg" width={} height={}>
...
...
<text className="svg-circle-text" x={} y={}>
...
</text>
</svg>
Add the below CSS styles into the ProgressBar.css
file and import it into the component.
.svg {
display: block;
margin: 20px auto;
max-width: 100%;
}
.svg-circle-bg {
fill: none;
}
.svg-circle {
fill: none;
}
.svg-circle-text {
font-size: 2rem;
text-anchor: middle;
fill: #fff;
font-weight: bold;
}
As you can see, we don’t have much in the way of CSS styles. The progress bar elements will contain properties that will add some styles to the elements. Let’s take a closer look.
Progress component props
The progress bar component takes in five props:
-
size
— the full width and height of the SVG -
progress
— the circular progress value -
strokeWidth
— the width (thickness) of the circles -
circleOneStroke
— the stroke color of first circle -
circleTwoStroke
— the stroke color of the second circle
These properties will be passed in as props into the circular progress component when it is used. Other properties, such as radius
and circumference
, are calculated from the provided props.
Pass a props property into the arrow function and destructure the five properties.
const ProgressBar = (props) => {
const {
size,
progress,
strokeWidth,
circleOneStroke,
circleTwoStroke,
} = props;
...
}
Next, calculate the radius and circumference of the circles. Add a new variable called center
and set its value to half of the size passed in as props. This value will be used in the cx
and cy
coordinates of the center of circle.
const center = size / 2;
The radius of the path is defined to be in the middle, so for the path to fit perfectly inside the viewBox, we need to subtract half the strokeWidth
from half the size (diameter). The circumference of the circle is 2 * π * r
.
const radius = size / 2 - strokeWidth / 2;
const circumference = 2 * Math.PI * radius;
Add the props and radius to the SVG and circles.
<svg className="svg" width={size} height={size}>
<circle
className="svg-circle-bg"
stroke={circleOneStroke}
cx={center}
cy={center}
r={radius}
strokeWidth={strokeWidth}
/>
<circle
className="svg-circle"
stroke={circleTwoStroke}
cx={center}
cy={center}
r={radius}
strokeWidth={strokeWidth}
/>
<text className="svg-circle-text" x={center} y={center}>
{progress}%
</text>
</svg>
Go to the App.js
file and import the ProgressBar
component. Add the component inside the div element with class name app-header
.
const App = () => {
return (
<div className="app">
<div className="app-header">
<ProgressBar />
</div>
</div>
);
}
Back to the ProgressBar.js
file. The ProgressBar
component needs to have the props defined inside its component.
<ProgressBar
progress={50}
size={500}
strokeWidth={15}
circleOneStroke='#7ea9e1'
circleTwoStroke='#7ea9e1'
/>
The user can specify values for the properties. Later, the progress value will be updated from a button click and an input. The circleTwoStroke
value will be randomly selected from an array of colors.
When using SVG, there are ways to control how strokes are rendered. Let’s take a look at stroke-dasharray
and stroke-dashoffset
.
stroke-dasharray
enables you to control the length of the dash and the spacing between each dash. Basically, it defines the pattern of dashes and gaps that is used to paint the outline of the shape — in this case, the circles.
Rather than creating multiple dashes, we can create one big dash to go around the entire circle. We’ll do this using the circumference we calculated earlier. The stroke-dashoffset
will determine the position from where the rendering starts.
The second circle will display the progress value between 0 and 100. Add the property below to the second circle
strokeDasharray={circumference}
Notice that we are using strokeDasharray
and not stroke-dasharray
. In react, css properties that are separated by -
are usually written in camelCase when used inside the component.
...
<circle
className="svg-circle"
stroke={circleTwoStroke}
cx={center}
cy={center}
r={radius}
strokeWidth={strokeWidth}
strokeDasharray={circumference}
/>
...
We’ll use three different React hooks: useState
, useEffect
, and useRef
. useState
updates the stroke-dashoffset
based on the progress value passed as a prop and inside the
useEffect
hook. The useRef
hook will be used to get a reference to the second circle and then
add a CSS transition property to the circle.
Import the useState
, useEffect
, and useRef
hooks from React.
import React, { useEffect, useState, useRef } from 'react';
Create a new useState
property inside the arrow function and set its default value to zero.
const [offset, setOffset] = useState(0);
On the second circle, add a ref
property and then create a new variable after the useState
property.
...
<circle
...
ref={circleRef}
...
/>
...
const circleRef = useRef(null);
The circleRef
property will produce a reference to the second circle, and then we can update its style on the DOM.
Next, add a useEffect
method
useEffect(() => {
}, []);
Inside the useEffect
hook, calculate the position of the progress by using this formula:
((100 - progress) / 100) * circumference;
Recall that the circumference is already calculated and the progress is a prop value set by the user.
useEffect(() => {
const progressOffset = ((100 - progress) / 100) * circumference;
setOffset(progressOffset);
}, [setOffset, circumference, progress, offset]);
The properties inside the array are dependencies and so must be added to the useEffect array.
After calculating the progressOffset, the setOffset
method is used to update the offset
.
Add to the second circle:
...
<circle
...
strokeDashoffset={offset}
...
/>
...
It should look like the screenshots below.
70 percent progress:
30 percent progress:
To add some transition to the stroke-dashoffset
, we’ll use useRef
, which has been defined. The useRef
hook gives us access to the current
property of the element on the DOM, which enables us to access the style property. We’ll place this transition inside the useEffect
hook so that it will be rendered as soon as the progress value changes.
Below the setOffset
method and inside the useEffect
hook, add:
circleRef.current.style = 'transition: stroke-dashoffset 850ms ease-in-out;';
circleRef
is the variable defined for useRef
, and we have access to its current and style properties. To see the change, reload your browser and observe how the transition occurs.
We now have our progress bar component. Let’s add some prop-types to the component.
import PropTypes from 'prop-types';
This places the prop-types definition right before the export default.
ProgressBar.propTypes = {
size: PropTypes.number.isRequired,
progress: PropTypes.number.isRequired,
strokeWidth: PropTypes.number.isRequired,
circleOneStroke: PropTypes.string.isRequired,
circleTwoStroke: PropTypes.string.isRequired
}
These properties are defined as required properties. You can add more properties to the component if you wish.
Your ProgressBar
functional component should look like this:
import React, { useEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import './ProgressBar.css';
const ProgressBar = props => {
const [offset, setOffset] = useState(0);
const circleRef = useRef(null);
const {
size,
progress,
strokeWidth,
circleOneStroke,
circleTwoStroke,
} = props;
const center = size / 2;
const radius = size / 2 - strokeWidth / 2;
const circumference = 2 * Math.PI * radius;
useEffect(() => {
const progressOffset = ((100 - progress) / 100) * circumference;
setOffset(progressOffset);
circleRef.current.style = 'transition: stroke-dashoffset 850ms ease-in-out;';
}, [setOffset, circumference, progress, offset]);
return (
<>
<svg
className="svg"
width={size}
height={size}
>
<circle
className="svg-circle-bg"
stroke={circleOneStroke}
cx={center}
cy={center}
r={radius}
strokeWidth={strokeWidth}
/>
<circle
className="svg-circle"
ref={circleRef}
stroke={circleTwoStroke}
cx={center}
cy={center}
r={radius}
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={offset}
/>
<text
x={`${center}`}
y={`${center}`}
className="svg-circle-text">
{progress}%
</text>
</svg>
</>
)
}
ProgressBar.propTypes = {
size: PropTypes.number.isRequired,
progress: PropTypes.number.isRequired,
strokeWidth: PropTypes.number.isRequired,
circleOneStroke: PropTypes.string.isRequired,
circleTwoStroke: PropTypes.string.isRequired
}
export default ProgressBar;
Generate random progress values
To see the transition applied to the progress, we’ll create an input field to enable the user to change progress values and a button to add random progress values.
Start by adding the below CSS styles to the App.css
file.
button {
background: #428BCA;
color: #fff;
font-size: 20px;
height: 60px;
width: 150px;
line-height: 60px;
margin: 25px 25px;
text-align: center;
outline: none;
}
input {
border: 1px solid #666;
background: #333;
color: #fff !important;
height: 30px;
width: 200px;
outline: none !important;
text-align: center;
font-size: 16px;
font-weight: bold;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
h1 {
margin: 0;
text-transform: uppercase;
text-shadow: 0 0 0.5em #fff;
font-size: 46px;
margin-bottom: 20px;
}
The styles are basic for button, input, and h1 elements. Next, add some elements to the div with class name app-header
.
<h1>SVG Circle Progress</h1>
<ProgressBar
progress={50}
size={500}
strokeWidth={15}
circleOneStroke='#7ea9e1'
circleTwoStroke='#7ea9e1'
/>
<p>
<input
type="number"
name="percent"
placeholder="Add Progress Value"
onChange={}
/>
</p>
<button>
Random
</button>
This adds s header
tag, p
tag with input, and a button. Let’s add the onChange method
to the input.
...
...
<p>
<input
type="number"
name="percent"
placeholder="Add Progress Value"
onChange={onChange}
/>
</p>
...
const onChange = e => {
}
Inside the onChange
method, the progress value and a random color will be selected and their
properties updated. Import useState
and create a useState
property called progress
.
const [progress, setProgress] = useState(0);
Create a useState
color property.
const [color, setColor] = useState('');
Add an array of colors with hex codes. You can set whatever colors you wish.
const colorArray = ['#7ea9e1', "#ed004f", "#00fcf0", "#d2fc00", "#7bff00", "#fa6900"];
A random color will be selected from the array and displayed on the circular progress component.
Update the ProgressBar
component with the progress
and color
props.
<ProgressBar
progress={progress}
size={500}
strokeWidth={15}
circleOneStroke='#7ea9e1'
circleTwoStroke={color}
/>
Add a method that gets a random color from the colorArray
.
const randomColor = () => {
return colorArray[Math.floor(Math.random() * colorArray.length)];
}
Set the maximum value for the progress component to 100 and the minimum value to 0. If the
input value is less than zero, the progress is set to zero. If it’s greater than 100,
the progress is set to 100.
if (e.target.value) {
if (e.target.value > 100) {
progress = 100;
}
if (e.target.value < 0) {
progress = 0;
}
setProgress(progress);
}
The setProgress
method will update the progress value. Add the randomColor
method below the setProgress
and update the color variable using setColor
.
...
const randomProgressColor = randomColor();
setColor(randomProgressColor);
If you try this out, you’ll discover it works but if the input field is empty, it still retains some old
value. This is not the behavior we want. To fix this, I’ll add an else statement inside the onChange
and set the progress value to zero.
if (e.target.value) {
...
} else {
setProgress(0);
}
This will set the progress value to zero whenever the input field is cleared or empty.
Random button functionality
Add an onClick
method on the button and create a function to randomly set the progress value.
<button onClick={randomProgressValue}>
Random
</button>
Create a method called randomProgressValue
.
const randomProgressValue = () => {
}
First, use Math.random()
to get a random value between 0 and 100 and set its value with the setProgress
method. The randomColor
method is called and the color value is updated.
const randomProgressValue = () => {
const progressValue = Math.floor(Math.random() * 101);
setProgress(progressValue);
const randomProgressColor = randomColor();
setColor(randomProgressColor);
}
Whenever the button is clicked, a random progress value is set and a random color is added using the setColor
method.
Note that using a random colors array is optional. You can set any two colors you want for the circleOneStroke
and circleTwoStroke
props.
Conclusion
You should now have a good understanding of how to create a custom circular progress bar using React hooks such as useState
, useEffect
, and useRef
.
See the full source code for this tutorial in the GitHub repo.
If you prefer to watch me as I code, you can check out this YouTube video here.
Full visibility into production React apps
Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
The post How to build an SVG circular progress component using React and React Hooks appeared first on LogRocket Blog.
Top comments (0)