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.

html
<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 + "%"; and progressBar.style.width = progress + "%";: update the label and the width of the bar.
  • clearInterval(interval);: stops the timer once the progress reaches 100%.
js
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.
html
<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.
html
<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