Tutorials / / 4 min read
How to create a progress bar with Tailwind CSS: JavaScript and Alpine.js
Build an animated progress bar with Tailwind CSS, two ways: vanilla JavaScript or Alpine.js. A timer fills the bar to 100% with a live percentage label.
Today we are building an animated progress bar: a timer fills it up to 100% while a label shows the percentage. We’ll build it twice, first with vanilla JavaScript, then with Alpine.js, so you can pick whichever fits your stack.
What is a progress bar?
A progress bar shows your users how far along something is, so they are not left staring at a frozen screen. Ours fills from 0% to 100% on a timer, with the width driven by the progress value and Tailwind handling the looks.
Use cases
- File uploads: show how much of the file has been sent.
- Downloads: reassure users that the transfer is moving along.
- Form processing: indicate that a submission is being worked on.
- Reading progress: on a blog post, show how much is left to read.
The markup
id="progress-container": the container element, used to identify the component in JavaScript.id="progress-text": the text element we update with the current progress value.id="progress-bar": the bar itself, we update its width as the progress grows.class="absolute top-0 left-0 h-full bg-orange-500 rounded-full": the Tailwind classes that style the bar.
Classes are removed for brevity.
<div id="progress-container">
<div id="progress-text">0%</div>
<div>
<div
id="progress-bar"
class="absolute top-0 left-0 h-full bg-orange-500 rounded-full"
style="width: 0%;"
></div>
</div>
</div>The script
let progress = 0;: the variable that stores the progress value and drives the bar.const interval = setInterval(() => {: runs every 100ms to update the bar.if (progress < 100) {: while there is room left,progress += 5;increments the value.progressText.textContent = progress + "%";andprogressBar.style.width = progress + "%";: update the label and the width of the bar.clearInterval(interval);: stops the timer once the progress reaches 100%.
document.addEventListener("DOMContentLoaded", function () {
let progress = 0;
const progressText = document.getElementById("progress-text");
const progressBar = document.getElementById("progress-bar");
const interval = setInterval(() => {
if (progress < 100) {
progress += 5;
progressText.textContent = progress + "%";
progressBar.style.width = progress + "%";
} else {
clearInterval(interval);
}
}, 100);
});The Alpine.js version
Same component, no separate script: the state lives in x-data.
x-data="{ progress: 0, interval: null }": stores the progress value and the interval.x-init="() => { interval = setInterval(() => { progress < 100 ? progress += 5 : clearInterval(interval); }, 100); }": runs every 100ms, incrementing the progress until it reaches 100.x-text="progress + '%'": displays the progress value.x-bind:style="'width: ' + progress + '%;'": sets the width of the progress bar.
<div
x-data="{ progress: 0, interval: null }"
x-init="() => { interval = setInterval(() => { progress < 100 ? progress += 5 : clearInterval(interval); }, 100); }"
>
<div x-text="progress + '%'"></div>
<div>
<div x-bind:style="'width: ' + progress + '%;'"></div>
</div>
</div>And here it is with the Tailwind classes on:
w-full: the width of the container.text-sm text-zinc-500: the styling of the percentage label.relative h-2 mt-2 rounded-full bg-zinc-200: the track, sized, rounded, and tinted.absolute top-0 left-0 h-full bg-orange-500 rounded-full: the bar itself.
<div
x-data="{ progress: 0, interval: null }"
x-init="() => { interval = setInterval(() => { progress < 100 ? progress += 5 : clearInterval(interval); }, 100); }"
class="w-full"
>
<div class="text-sm text-zinc-500" x-text="progress + '%'"></div>
<div class="relative h-2 mt-2 rounded-full bg-zinc-200">
<div
x-bind:style="'width: ' + progress + '%;'"
class="absolute top-0 left-0 h-full bg-orange-500 rounded-full"
></div>
</div>
</div>You can try the Alpine version separately: live demo and source code.
Which one should you use?
If Alpine is already in your project, x-init gives you the timer without any extra script. Otherwise the vanilla version is just as short and dependency-free.
Conclusion
A value, a timer, and a width binding: that’s all a progress bar needs. Swap the fake interval for real upload or scroll progress and you are set.
Hope you enjoyed this tutorial and have a great day!
/Michael Andreuzza