🎄 Get 20% off from our JavaScript course for the holidays! 🎄
💡 This page contain affiliate links. By making a purchase through them, we may earn a commission at no extra cost to you.
How to Make an Animated Progress Bar in CSS

How to Make an Animated Progress Bar in CSS

With the power of CSS transitions
Ferenc AlmasiLast updated 2021 November 11 • Read time 10 min read
Learn how you can make a progress bar animation entirely in CSS, with the help of JavaScript to also track progress.
  • twitter
  • facebook
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.

progress bar created with CSS
The final end result

Creating the Markup for the Progress Bar

To start things off, create three separate files: index.htmlstyles.css, and app.js.

Project structure
The structure of the project

Open your index.html file and add the following elements to it:

Copied to clipboard! Playground
<!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>
index.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:

Copied to clipboard! Playground
html {
    background: #252535;
    font-family: 'Lobster', cursive;
}

.progress-button {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
}
styles.css

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.

how translate helps to position the button to dead center

We also want to add some styles for the button:

Copied to clipboard! Playground
.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);
}
styles.css

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.
importance of box-sizing and cursor

Now its time to add styles for the other texts:

Copied to clipboard! Playground
.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);
}
styles.css

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.

With and without z-index
Looking to improve your skills? Master CSS from start to finish.
Master CSS Remove ads

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.

Copied to clipboard!
document.querySelector('.download-button').onclick = () => {
    document.querySelector('.progress-button').classList.add('in-progress');
}
app.js

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:

Copied to clipboard! Playground
.in-progress .loading-text {
    opacity: 1;
    top: -55%;
}

.in-progress .percentage {
    opacity: 1;
    top: 155%;
}

.in-progress .button-text {
    transform: translateY(-35px);
    opacity: 0;
}
styles.css

These rules will animate the texts over 600ms.

Creating a sliding effect in CSS
This will create a sliding effect

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:

Copied to clipboard! Playground
.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;
}
styles.css

It uses a slower (+300ms) transition for the width of the element.

Increasing the width of the button with CSS transitions

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.

Copied to clipboard! Playground
.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);
}
styles.css

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.

Animating the stripes with its width

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.

Using `position: relative;` for the 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.

Copied to clipboard!
+ :root {
+    --progress: 0%;
+ }

  ...
  
 .download-button:after {
     ...
-    width: 0%;
+    width: var(--progress);
 }
styles.diff

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:

Copied to clipboard! Playground
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}%`;
        }
    })();
}
app.js

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.

Making progress by updating the CSS variable
Looking to improve your skills? Master CSS from start to finish.
Master CSS Remove ads

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:

Copied to clipboard! Playground
.finished .download-button {
    background: #70C542;
}

.finished .download-button:after {
    opacity: 0;
}
styles.css

Everything else can be handled inside the click event listener:

Copied to clipboard! Playground
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);
})();
app.js

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:

Copied to clipboard!
if (progress % 5 === 0) {
    documentStyles.setProperty('--progress', `${progress}%`);
    loadingText.innerHTML = `Loading${Array(progress % 4).fill('.').join('')}`;
}
app.js
progress bar created with CSS
The final end result

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.

10 Best Practices for Quickly Improving Your CSS
Did you find this page helpful?
📚 More Webtips
Frontend Course Dashboard
Master the Art of Frontend
  • check Access exclusive interactive lessons
  • check Unlimited access to hundreds of tutorials
  • check Remove ads to learn without distractions
Become a Pro

Courses

Recommended