On This Page
Demo App
This is what we are going to build in this tutorial. You can play by clicking Next button.
Almost every e-commerce app or website includes a progress steps component to indicate the flow or current step in a process. Many developers want to build their own custom version without using styled-components or UI libraries like Material UI or Chakra.
In this article, we’ll build a reusable, fully functional progress steps component using ReactJS and vanilla CSS, step by step.
If you're looking for the Tailwind CSS version, copy the code from this Tailwind CSS stepper component.
Let’s Get Started
It needs to be coded in a way that it becomes reusable in any web app. The approach I use while building web apps is to design a UI first and then implement a logic.
Design the Basic Step UI
Step 01: Designing four steps
The step is just a circle. We can add a number or checkmark based on the state of the app, step will have a label too.
import './progress-steps.css'const ProgressSteps = () => {return (<div className="progress-steps__item"><div className="progress-steps__circle"><span className="progress-steps__count">1</span></div><div className="progress-steps__label-container"><span className="progress-steps__label">Address</span></div></div>)}export default ProgressSteps
CSS Style
.progress-steps__item {position: relative;z-index: 1;}.progress-steps__circle {width: 40px;height: 40px;border-radius: 50%;background-color: #ffffff;border: 3px solid #f3e7f3;transition: 0.4s ease;display: flex;justify-content: center;align-items: center;margin: 0 auto;}.progress-steps__count {font-size: 19px;color: #f3e7f3;}.progress-steps__label-container {position: absolute;top: 66px;left: 50%;transform: translate(-50%, -50%);}.progress-steps__label {font-size: 19px;color: #4a154b;}

First Trick
You might have noticed we have made the position of step label absolute
. It is to make it independent of the step circle (you will find the reason in step 2)
As we have four steps in our web app, so will map over them to build remaining steps.
import './progress-steps.css'const ProgressSteps = () => {return (<div className="progress-steps"><div className="progress-steps__container">{[1, 2, 3, 4].map(() => (<div className="progress-steps__item"><div className="progress-steps__circle"><span className="progress-steps__count">1</span></div><div className="progress-steps__label-container"><span className="progress-steps__label">Address</span></div></div>))}</div></div>)}export default ProgressSteps
CSS Style
.progress-steps {width: 100%;max-width: 600px;margin: 0 auto;padding: 0 16px;}.progress-steps__container {display: flex;justify-content: space-between;margin-top: 70px;position: relative;}.progress-steps__item {position: relative;z-index: 1;}.progress-steps__circle {width: 40px;height: 40px;border-radius: 50%;background-color: #ffffff;border: 3px solid #f3e7f3;transition: 0.4s ease;display: flex;justify-content: center;align-items: center;}.progress-steps__count {font-size: 19px;color: #f3e7f3;}.progress-steps__label-container {position: absolute;top: 66px;left: 50%;transform: translate(-50%, -50%);}.progress-steps__label {font-size: 19px;color: #4a154b;}
Meanwhile, we have also created two more div
for Step Container
for making it display: flex
and Main Container
to contain the whole component.
After doing it, we will have results something like this in our browser.

Making it dynamic
Is it the right approach? NO…
It is not the proper approach to build steps because what if a web app requires five or three steps? So, to make it completely dynamic and scalable, we will add a JavaScript array of objects
.
import './progress-steps.css'const steps = [{label: 'Address',step: 1,},{label: 'Shipping',step: 2,},{label: 'Payment',step: 3,},{label: 'Summary',step: 4,},]const ProgressSteps = () => {return (<div className="progress-steps"><div className="progress-steps__container">{steps.map(({ step, label }) => (<div className="progress-steps__item" key={step}><div className="progress-steps__circle"><span className="progress-steps__count">{step}</span></div><div className="progress-steps__label-container"><span className="progress-steps__label">{label}</span></div></div>))}</div></div>)}export default ProgressSteps

Step 02: Building a Progress Line
Next, we need a line over the steps to make it look connected.
It is pretty simple; we will use CSS pseudo-class property for that. So, we don’t add another element to our DOM.
.progress-steps__container {display: flex;justify-content: space-between;margin-top: 70px;position: relative;}.progress-steps__container:before {content: '';position: absolute;background: #f3e7f3;height: 4px;width: 100%;top: 50%;transform: translateY(-50%);left: 0;}
This step has nothing fancy. Here we have only used simple CSS to get this result.
We have positioned the line in the middle of our container using the top, transform, and left
properties and make it 100% of the container.

The last tricky step is here!
Right now, our progress line is dull and empty. We want to fill the portion of the line with a darker color whenever a step is done.
So, the trick here for having a color-filled progress line is adding another line over the previous one.
For this we have created another element in DOM with class progress-steps__filled-line
and made it position absolute
.
import './progress-steps.css'const steps = [{label: 'Address',step: 1,},{label: 'Shipping',step: 2,},{label: 'Payment',step: 3,},{label: 'Summary',step: 4,},]const ProgressSteps = () => {return (<div className="progress-steps"><div className="progress-steps__container">{steps.map(({ step, label }) => (<div className="progress-steps__item" key={step}><div className="progress-steps__circle"><span className="progress-steps__count">{step}</span></div><div className="progress-steps__label-container"><span className="progress-steps__label">{label}</span></div></div>))}<div className="progress-steps__filled-line" style={{ width: '33.3%' }}></div></div></div>)}export default ProgressSteps
Filled Line Style
.progress-steps__filled-line {content: '';position: absolute;background: #4a154b;height: 4px;width: 100%;top: 50%;transition: 0.4s ease;transform: translateY(-50%);left: 0;}
Is it hard to understand? No, we are only playing with CSS properties.
The width
is set to static 33.3% width
to see the result, but it will become dynamic after logic implementation.

Step 03: Navigation Buttons
Now we are left with Next
and Previous
buttons. This part is quite straightforward
import './progress-steps.css'const steps = [{label: 'Address',step: 1,},{label: 'Shipping',step: 2,},{label: 'Payment',step: 3,},{label: 'Summary',step: 4,},]const ProgressSteps = () => {return (<div className="progress-steps"><div className="progress-steps__container">{steps.map(({ step, label }) => (<div className="progress-steps__item" key={step}><div className="progress-steps__circle"><span className="progress-steps__count">{step}</span></div><div className="progress-steps__label-container"><span className="progress-steps__label">{label}</span></div></div>))}<div className="progress-steps__filled-line" style={{ width: '33.3%' }}></div></div><div className="progress-steps__buttons"><button className="progress-steps__button">Previous</button><button className="progress-steps__button">Next</button></div></div>)}export default ProgressSteps
Navigation Button Styles
.progress-steps__buttons {display: flex;justify-content: space-between;margin: 0 -15px;margin-top: 100px;}.progress-steps__button {border-radius: 4px;border: 0;background: #4a154b;color: #ffffff;cursor: pointer;padding: 8px;width: 90px;}.progress-steps__button:active {transform: scale(0.98);}.progress-steps__button:disabled {background: #f3e7f3;color: #000000;cursor: not-allowed;}
Our button has active
and disabled
classes just like buttons have.
The first milestone is achieved, and we are done with our structural and CSS parts. It’s time to write some logic.

Second, let’s Make it Functional
To make the progress components fully functional, we have to implement two logics, the first to keep track of our actions and the second to calculate the width of the progress bar.
Let us start with the logic of keeping track of our actions.
We need only one state to keep track of our action steps. It requires two functions
, one for the next
and another for moving to the previous
step.
import { useState } from 'react'import './progress-steps.css'const steps = [{label: 'Address',step: 1,},{label: 'Shipping',step: 2,},{label: 'Payment',step: 3,},{label: 'Summary',step: 4,},]const ProgressSteps = () => {const [activeStep, setActiveStep] = useState(1)const nextStep = () => {setActiveStep(activeStep + 1)}const prevStep = () => {setActiveStep(activeStep - 1)}const totalSteps = steps.lengthreturn (<div className="progress-steps"><div className="progress-steps__container">{steps.map(({ step, label }) => (<div className="progress-steps__item" key={step}><div className="progress-steps__circle"><span className="progress-steps__count">{step}</span></div><div className="progress-steps__label-container"><span className="progress-steps__label">{label}</span></div></div>))}<div className="progress-steps__filled-line" style={{ width: '33.3%' }}></div></div><div className="progress-steps__buttons"><button className="progress-steps__button" onClick={prevStep} disabled={activeStep === 1}>Previous</button><buttonclassName="progress-steps__button"onClick={nextStep}disabled={activeStep === totalSteps}>Next</button></div></div>)}export default ProgressSteps
We have added disabling button functionality with the logic: If activeState
is 1 previous button will be disabled, and on the last step, the Next button will be disabled.
We can change this logic while implementing it in a real-world app.
Now the last trick of this article is calculating the width
Our last trick or logic of this article is to calculate the width of the progress line.
Here is the formula:
const width = `${(100 / (totalSteps - 1)) * (activeStep - 1)}%`
If we increase or decrease steps, our progress line will not overflow or shorten than steps container because of this formula.
I learned this width calculation formula from Brad Traversy’s 50 Projects In 50 Days - HTML, CSS & JavaScript. Here you can find this course.
Dynamic width with inline styling
import React, { useState } from 'react'import './progress-steps.css' // Import your CSS fileconst steps = [{label: 'Address',step: 1,},{label: 'Shipping',step: 2,},{label: 'Payment',step: 3,},{label: 'Summary',step: 4,},]const ProgressSteps = () => {const [activeStep, setActiveStep] = useState(1)const nextStep = () => {setActiveStep(activeStep + 1)}const prevStep = () => {setActiveStep(activeStep - 1)}const totalSteps = steps.lengthconst width = `${(100 / (totalSteps - 1)) * (activeStep - 1)}%`return (<div className="progress-steps"><div className="progress-steps__container">{steps.map(({ step, label }) => (<div className="progress-steps__item" key={step}><divclassName={`progress-steps__circle ${activeStep >= step ? 'progress-steps__circle--completed' : ''}`}>{activeStep > step ? (<div className="progress-steps__checkmark">L</div>) : (<span className="progress-steps__count">{step}</span>)}</div><div className="progress-steps__label-container"><span className="progress-steps__label" key={step}>{label}</span></div></div>))}<div className="progress-steps__filled-line" style={{ width: width }}></div></div>{/* navigation buttons */}</div>)}export default ProgressSteps
We did three things in above code:
- We are passing width props as inline style, so it acts accordingly.
- Darken the step border if the step is active or done. So, in
Step Style
, we will set the logic as ifacitveStep
is equal to or greater than the step, it will passcompleted
styling, otherwiseincomplete
(just regular styling). - Insert a
checkmark
style, if the step is done, it will show the check mark inside the step; otherwise simple digit.
Finally, we are done with building reusable progress step component.
React Progress Step Component Complete Code
import React, { useState } from 'react'import './progress-steps.css' // Import your CSS fileconst steps = [{label: 'Address',step: 1,},{label: 'Shipping',step: 2,},{label: 'Payment',step: 3,},{label: 'Summary',step: 4,},]const ProgressSteps = () => {const [activeStep, setActiveStep] = useState(1)const nextStep = () => {setActiveStep(activeStep + 1)}const prevStep = () => {setActiveStep(activeStep - 1)}const totalSteps = steps.lengthconst width = `${(100 / (totalSteps - 1)) * (activeStep - 1)}%`return (<div className="progress-steps"><div className="progress-steps__container">{steps.map(({ step, label }) => (<div className="progress-steps__item" key={step}><divclassName={`progress-steps__circle ${activeStep >= step ? 'progress-steps__circle--completed' : ''}`}>{activeStep > step ? (<div className="progress-steps__checkmark">L</div>) : (<span className="progress-steps__count">{step}</span>)}</div><div className="progress-steps__label-container"><span className="progress-steps__label" key={step}>{label}</span></div></div>))}<div className="progress-steps__filled-line" style={{ width: width }}></div></div><div className="progress-steps__buttons"><button className="progress-steps__button" onClick={prevStep} disabled={activeStep === 1}>Previous</button><buttonclassName="progress-steps__button"onClick={nextStep}disabled={activeStep === totalSteps}>Next</button></div></div>)}export default ProgressSteps
Progress Step Component Styling
.progress-steps {width: 100%;max-width: 600px;margin: 0 auto;padding: 0 16px;}.progress-steps__container {display: flex;justify-content: space-between;margin-top: 70px;position: relative;}.progress-steps__container:before {content: '';position: absolute;background: #f3e7f3;height: 4px;width: 100%;top: 50%;transform: translateY(-50%);left: 0;}.progress-steps__filled-line {content: '';position: absolute;background: #4a154b;height: 4px;width: 100%;top: 50%;transition: 0.4s ease;transform: translateY(-50%);left: 0;}.progress-steps__item {position: relative;z-index: 1;}.progress-steps__circle {width: 40px;height: 40px;border-radius: 50%;background-color: #ffffff;border: 3px solid #f3e7f3;transition: 0.4s ease;display: flex;justify-content: center;align-items: center;}.progress-steps__circle--completed {border-color: #4a154b;}.progress-steps__count {font-size: 19px;color: #f3e7f3;}@media (max-width: 600px) {.progress-steps__count {font-size: 16px;}}.progress-steps__label-container {position: absolute;top: 66px;left: 50%;transform: translate(-50%, -50%);}.progress-steps__label {font-size: 19px;color: #4a154b;}@media (max-width: 600px) {.progress-steps__label {font-size: 16px;}}.progress-steps__buttons {display: flex;justify-content: space-between;margin: 0 -15px;margin-top: 100px;}.progress-steps__button {border-radius: 4px;border: 0;background: #4a154b;color: #ffffff;cursor: pointer;padding: 8px;width: 90px;}.progress-steps__button:active {transform: scale(0.98);}.progress-steps__button:disabled {background: #f3e7f3;color: #000000;cursor: not-allowed;}.progress-steps__checkmark {font-size: 26px;font-weight: 600;color: #4a154b;-ms-transform: scaleX(-1) rotate(-46deg); /* IE 9 */-webkit-transform: scaleX(-1) rotate(-46deg); /* Chrome, Safari, Opera */transform: scaleX(-1) rotate(-46deg);}