
How to Make an Animated Progress Bar in CSS
When thinking about user experience, one commonly occurring problem is not giving feedback to user input. Any action that your users take should cause a reaction. If your application has long-running tasks, itās essential to show progress. Otherwise, users will be left confused.
Did I click the button? Did it break? Did my connection went down? Is it finished already? Why itās taking so long? Iām leavingā¦
In this tutorial, we will focus on how CSS transitions can be used to create an animated progress bar. I will leave a link to the GitHub repository at the end of the article where the final project is hosted. This is how the progress bar will look like.

Creating the Markup for the Progress Bar
To start things off, create three separate files:Ā index.html
,Ā styles.css
, andĀ app.js
.

Open yourĀ index.html
Ā file and add the following elements to it:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>š„ Animated Progress Bar With CSS</title>
<link href="https://fonts.googleapis.com/css2?family=Lobster&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="progress-button">
<span class="loading-text">Loading</span>
<button class="download-button">
<span class="button-text">Download</span>
</button>
<span class="percentage">0%</span>
</div>
<script src="app.js"></script>
</body>
</html>
This is all that we are going to work with. We will have the whole thing wrapped in aĀ .progress-button
div
. We will change the copy of all three elements through JavaScript. This will be the only purpose ofĀ app.js
, as well as handling the click event.
Two other things to point out: Iām usingĀ LobsterĀ from Google Fonts. The text of the button should be wrapped in aĀ span
Ā as we will animate that as well. Now letās jump into styling them.
Styling the Progress Bar
We will position the button dead center. To achieve this, start with these two CSS selectors:
html {
background: #252535;
font-family: 'Lobster', cursive;
}
.progress-button {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
The trick here is usingĀ transform: translate(-50%, -50%)
. Since the anchor of the button is at the top left corner, we want to move it back half its width and height.

We also want to add some styles for the button:
.download-button {
width: 150px;
border-radius: 25px;
border: 0;
padding: 10px 40px;
font-family: 'Lobster';
font-size: 21px;
box-sizing: content-box;
cursor: pointer;
overflow: hidden;
box-shadow: 0px 5px 10px rgba(33, 33, 33, .25), inset 0 5px 10px rgba(33, 33, 33, .25);
}
.button-text {
display: inline-block;
opacity: 1;
color: #252535;
transition: all .6s cubic-bezier(0.4, 0.0, 0.2, 1);
}
There are five important things you should keep in mind.
- SetĀ
box-sizing
Ā toĀcontent-box
Ā so paddings are calculated the right way. - SetĀ
cursor
Ā toĀpointer
to indicate that the element is interactive. - SetĀ
overflow
Ā toĀhidden
. This will prevent the text of the button to be visible once itās outside the buttonās body. - You can set multipleĀ
box-shadow
Ā values by comma separating them. - The text of the button needs to beĀ
inline-block
Ā as we will make positional transitions on it. You also need to setĀtransition
Ā to create a smooth animation. Iām using 600ms to finish the transition.

Now its time to add styles for the other texts:
.loading-text,
.percentage {
display: block;
color: #70C542;
font-size: 32px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: -1;
opacity: 0;
transition: all .6s cubic-bezier(0.4, 0.0, 0.2, 1);
}
Just as for the button, we need to position it absolutely because we want to animate it from the middle of the button. You will also need to setĀ z-index
Ā to -1 to bring it behind the button, otherwise, it will overlap with it.


Animating the Texts
Now we have all the initial styles ready. Itās time to animate things. The animation starts by sliding out theĀ āLoadingāĀ text and percentage indicator. Concurrently, we also want to slide out the text of the button. To do all of this, we will add a class to the container once the button is clicked.
document.querySelector('.download-button').onclick = () => {
document.querySelector('.progress-button').classList.add('in-progress');
}
Now we can attach styles to the elements if their parent has anĀ .in-progress
Ā class. Extend yourĀ styles.css
Ā file with the following three selectors:
.in-progress .loading-text {
opacity: 1;
top: -55%;
}
.in-progress .percentage {
opacity: 1;
top: 155%;
}
.in-progress .button-text {
transform: translateY(-35px);
opacity: 0;
}
These rules will animate the texts over 600ms.

Animating the Button
The next step is to animate the button. We want to double its width to create a feel like we turned the button into a progress bar. For this, modify yourĀ styles.css
Ā according to the following:
.download-button {
width: 150px;
border-radius: 25px;
border: 0;
padding: 10px 40px;
font-family: 'Lobster';
font-size: 21px;
box-sizing: content-box;
cursor: pointer;
overflow: hidden;
transition: width .9s cubic-bezier(0.4, 0.0, 0.2, 1);
box-shadow: 0px 5px 10px rgba(33, 33, 33, .25), inset 0 5px 10px rgba(33, 33, 33, .25);
}
.in-progress .download-button {
width: 300px;
}
It uses a slower (+300ms) transition for theĀ width
Ā of the element.

Animating Stripes
To create the stripes, we can use theĀ ::after
Ā pseudo-element of theĀ button
. We will use aĀ linear-gradient
Ā as the background image to create the stripes.
.download-button:after {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 0%;
background-image: linear-gradient(-45deg, #92CE72 25%, #70C542 25%, #70C542 50%, #92CE72 50%, #92CE72 75%, #70C542 75%, #70C542);
background-size: 50px 50px;
border-radius: 25px;
transition: width .6s cubic-bezier(0.4, 0.0, 0.2, 1);
}
Changing the degree to -45° and changing colors every 25% creates the desired effect. We can simply increase progress by increasing the width of the element. Donāt forget to set a fixedĀ background-size
, otherwise, the stripes will get thicker as the width increases.

You may have noticed that when itās near 1%, the element looks squashed. This is because, for small widths, you canāt have a largeĀ border-radius
. And since the element overflows the button, the distortion is visible. To fix this, addĀ position: relative
Ā to yourĀ .download-button
.

Making Progress
To see it in action, letās create a function that simulates a download. Thereās only one problem. You canāt change āĀ at the time of writing this articleĀ ā the style of a pseudo-element through JavaScript. To work around this, we will useĀ CSS variables.
+ :root {
+ --progress: 0%;
+ }
...
.download-button:after {
...
- width: 0%;
+ width: var(--progress);
}
Add a new variable for theĀ :root
Ā element and use it in yourĀ ::after
. We will change the value of this through JavaScript. Open yourĀ app.js
Ā file and add the following:
const simulateDownload = (progress) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(progress + 1);
}, 50);
}).then(res => res);
};
document.querySelector('.download-button').onclick = () => {
document.querySelector('.progress-button').classList.add('in-progress');
const documentStyles = document.documentElement.style;
const percentage = document.querySelector('.percentage');
(async () => {
let progress = 0;
while (progress < 100) {
progress = await simulateDownload(progress);
if (progress % 5 === 0) {
documentStyles.setProperty('--progress', `${progress}%`);
}
percentage.innerText = `${progress}%`;
}
})();
}
Only line:9ā10 remained the same. The new function, defined on line:1 will resolve a new Promise every 50ms. We can then have a loop that goes from 0 to 100 and increaseĀ progress
by one in each iteration.
This is where we can rewrite the CSS variable usingĀ setProperty
Ā on line:22. The if statement is for updating the property only every 5%. If we try this out now, we should have a loading animation.


Finishing Up
All thatās left to do is to indicate to the user when the download has finished. To do this, letās switch theĀ .in-progress
Ā class toĀ .finished
. We will need two more styles for this:
.finished .download-button {
background: #70C542;
}
.finished .download-button:after {
opacity: 0;
}
Everything else can be handled inside the click event listener:
const progressButton = document.querySelector('.progress-button');
const buttonText = document.querySelector('.button-text');
progressButton.classList.add('in-progress');
(async () => {
let progress = 0;
while (progress < 100) {
...
}
buttonText.innerText = 'š Done';
setTimeout(() => progressButton.classList.replace('in-progress', 'finished'), 1000);
})();
Iāve also outsourced theĀ .progress-button
Ā selector into a variable as we use it multiple times. At line:14, theĀ .in-progress
Ā class is switched toĀ .finished
. Because we remove it, we donāt have to worry about repositioning the texts. They will return to their initial positions.
If you would also like to update theĀ āLoadingāĀ text with ellipsis, add this line to yourĀ app.js
:
if (progress % 5 === 0) {
documentStyles.setProperty('--progress', `${progress}%`);
loadingText.innerHTML = `Loading${Array(progress % 4).fill('.').join('')}`;
}

Summary
With the use of CSS transitions, you can create pretty complex animations. Apart from a few exceptions, you can transition almost any CSS property. Now your users will have no problem understanding whatās going on.
If you would like to tweak around with the finished project, you can clone it fromĀ this GitHub repository. Thank you for taking the time to read this article, happy styling! šØ
If you would like to learn more about CSS, check out what are some steps you can take to quickly improve the architecture of your stylesheets.

Access exclusive interactive lessons
Unlimited access to hundreds of tutorials
Remove ads to learn without distractions
Courses

CSS - The Complete Guide (including Flexbox, Grid and Sass)

The HTML & CSS Bootcamp
