HTML Progress Bar CSS: How to Style the Progress Tag in HTML
HTML Progress Bar CSS: How to Style the Progress Tag in HTML

HTML Progress Bar CSS: How to Style the Progress Tag in HTML

Timonwa Akintokun Timonwa Akintokun

13 min read


A styled HTML Progress Bar
A styled HTML Progress Bar

In the world of web development, user experience (UX) is paramount. One small yet significant element that often goes overlooked is the humble progress bar. Whether you are building a simple CSS loading spinner, a file uploader, a multi-step form, or a loading state for a dashboard, providing visual feedback to your users is essential. It lets them know that something is happening behind the scenes and, more importantly, how much longer they might have to wait.

While the HTML5 <progress> element provides a native way to implement this functionality, its default appearance is—to put it mildly—a bit lackluster. It often looks dated and varies wildly across different browsers. In this guide, I will walk you through everything you need to know about styling the HTML progress bar, from basic attributes to advanced cross-browser CSS techniques.

TL;DR

  • Use the native <progress> element for standard tasks and better accessibility.
  • Reset default browser styles using appearance: none before applying custom CSS.
  • Target ::-webkit-progress-bar and ::-webkit-progress-value for WebKit browsers (Chrome, Safari).
  • Target <progress> and use ::-moz-progress-bar for Firefox value styling.
  • For high-brand customization, consider a custom div-based progress bar with ARIA roles.

Prerequisites

  • Basic understanding of HTML and CSS.
  • Familiarity with CSS pseudo-elements is helpful but not required.

Understanding the HTML Progress Element Basics

Before we dive into the colorful world of CSS, let’s take a moment to understand the foundation. The <progress> element is designed to represent the completion progress of a task. Unlike the <meter> element, which is used for scalar values within a known range (like disk usage or temperature), the <progress> element is strictly for showing how far along a process is.

Syntax and Attributes

The syntax is straightforward. You only need two primary attributes to get started:

  1. value: This determines the current progress.
  2. max: This defines the total amount of work to be done. The default value is one if this attribute is omitted.
<label for="file-progress">File Upload Progress:</label>
<progress id="file-progress" value="70" max="100">70%</progress>

By default, if you do not provide a value attribute, the progress bar enters an indeterminate state, which usually manifests as a continuous animation indicating that work is in progress but the time to completion is unknown.

<label for="upload-status">Progress bar without a value:</label>
<progress id="upload-status" max="100"></progress>

Two progress bars side by side, one with a value of 70% showing a mostly filled bar, and another without a value showing an indeterminate animation.

Pro Tip

Always include a fallback value inside the <progress> tag (like the “70%” in the example above). This ensures that older browsers that do not support the element can still display the numeric value to the user.

Solving the HTML Progress Bar Styling Challenge Across Browsers

If you have ever tried to style a <progress> element with simple background-color or border properties, you likely found yourself frustrated. This is because browsers render this element using their internal shadow DOM and specific styling engines.

Each browser has its own way of rendering the progress bar. Chrome and Safari use WebKit styles, while Firefox uses its own Gecko engine. This leads to a situation where your carefully crafted styles might look great in one browser but completely vanish in another. To truly master styling this element, we must venture into the realm of vendor-specific pseudo-classes.

An illustration comparing how the same HTML progress element renders differently in Chrome (blue, rounded), Firefox (green, square), and Safari (thin, blue/grey) browsers.
Browser rendering inconsistencies for the native progress element

How to Style a Progress Bar in HTML with CSS

To gain full control over the appearance of your progress bar, you first need to strip away the default browser styling. We do this by setting appearance to none.

Resetting the Appearance

To start from a clean slate, apply the following CSS to your progress element:

progress {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  border: none; /* Remove default border in Firefox */
  width: 100%;
  height: 20px;
}

Once the reset is in place, we can start adding our custom flavors. However, we have to target the “container” and the “value bar” separately for each browser engine.

Webkit Styling (Chrome, Safari, Edge)

For browsers like Chrome and Safari, you need to use two specific WebKit pseudo-elements:

  • ::-webkit-progress-bar: Targets the entire background of the progress element.
  • ::-webkit-progress-value: Targets the actual progress indicator (the part that fills up).
/* Container styles for WebKit */
progress::-webkit-progress-bar {
  background-color: #eee;
  border-radius: 50px;
  box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.1);
}

/* Progress bar value styles for WebKit */
progress::-webkit-progress-value {
  background-color: #8b0000;
  border-radius: 50px;
  transition: width 0.3s ease;
}

Firefox Styling

Firefox is a bit different. It uses the progress selector itself for the background and ::-moz-progress-bar for the value.

/* Container styles for Firefox */
progress {
  background-color: #eee;
  border-radius: 50px;
}

/* Progress bar value styles for Firefox */
progress::-moz-progress-bar {
  background-color: #8b0000;
  border-radius: 50px;
}

Enjoying this technical deep dive?

I share detailed guides like this every month. Join Bits & Notes to stay ahead of the curve!

HTML Progress Bar Color: Dynamic Styling with CSS and JavaScript

One of the most common requirements is changing the progress bar’s color based on its value—for instance, red for low, yellow for medium, and green for high. To do this correctly, you must include the base styling (reset) and target the specific pseudo-elements for each state and use JavaScript to toggle the classes based on the progress value:

The HTML

<div class="dynamic-color-wrapper">
  <progress
    id="color-progress"
    class="status-mid"
    value="50"
    max="100"
  ></progress>
  <div class="btn-group">
    <button onclick="setTheme('low')">Set Low (Red)</button>
    <button onclick="setTheme('mid')">Set Mid (Yellow)</button>
    <button onclick="setTheme('high')">Set High (Green)</button>
  </div>
</div>

The CSS

/* Note: You must always include the base reset for custom styles to apply */
#color-progress {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  width: 100%;
  height: 20px;
  border-radius: 10px;
  background-color: #eee;
}

/* WebKit states */
.status-low::-webkit-progress-value {
  background-color: #ff4c4c;
}
.status-mid::-webkit-progress-value {
  background-color: #ffcc00;
}
.status-high::-webkit-progress-value {
  background-color: #4caf50;
}

/* Firefox states */
.status-low::-moz-progress-bar {
  background-color: #ff4c4c;
}
.status-mid::-moz-progress-bar {
  background-color: #ffcc00;
}
.status-high::-moz-progress-bar {
  background-color: #4caf50;
}

The JavaScript

function setTheme(level) {
  const bar = document.getElementById("color-progress");
  bar.classList.remove("status-low", "status-mid", "status-high");
  bar.classList.add("status-" + level);
}

HTML Progress Bar Animation with CSS Transitions and Stripes Effect

Static progress bars are functional, but animated ones are engaging. Adding a smooth transition ensures that when the value attribute changes, the bar glides to its new position rather than jumping instantly.

Creating an Animating Striped Effect

To make the stripes actually move, you need to define @keyframes and apply them to the background-position of the progress value.

The HTML

<progress id="striped-bar" value="75" max="100"></progress>

The CSS

#striped-bar {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  width: 100%;
  height: 25px;
  border-radius: 15px;
}

/* WebKit Styling */
#striped-bar::-webkit-progress-bar {
  background-color: #f3f3f3;
  border-radius: 15px;
}

#striped-bar::-webkit-progress-value {
  border-radius: 15px;
  background-color: #3b82f6;
  background-image: linear-gradient(
    -45deg,
    rgba(255, 255, 255, 0.2) 25%,
    transparent 25%,
    transparent 50%,
    rgba(255, 255, 255, 0.2) 50%,
    rgba(255, 255, 255, 0.2) 75%,
    transparent 75%,
    transparent
  );
  background-size: 40px 40px;
  /* This makes the stripes move */
  animation: move-stripes 2s linear infinite;
  transition: width 0.4s ease;
}

/* The Animation */
@keyframes move-stripes {
  from {
    background-position: 0 0;
  }
  to {
    background-position: 40px 0;
  }
}

/* Firefox Support */
#striped-bar::-moz-progress-bar {
  background-color: #3b82f6;
  border-radius: 15px;
}

Create a Custom Progress Bar in HTML and CSS

While the <progress> tag is great for accessibility and simplicity, sometimes you just need more control than the native element allows. If you find yourself fighting with browser prefixes for hours, building a custom progress bar using two <div> elements might be the right path for you. This is also a great opportunity to create your own custom code snippets for your future projects.

The HTML

<div
  class="custom-progress"
  role="progressbar"
  aria-valuenow="45"
  aria-valuemin="0"
  aria-valuemax="100"
>
  <div class="custom-fill" style="width: 45%;">
    <span class="label">45% Complete</span>
  </div>
</div>

The CSS

.custom-progress {
  width: 100%;
  height: 30px;
  background-color: #eee;
  border-radius: 20px;
  overflow: hidden;
  position: relative;
}

.custom-fill {
  background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: width 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}

.label {
  color: white;
  font-weight: bold;
  font-size: 13px;
}

This approach gives you total freedom. You can add text labels inside the bar, use more complex gradients, or even add Box Shadows and internal glows without worrying about vendor prefixes.

Update an HTML Progress Bar Dynamically with JavaScript

For real-world applications like file uploads, you need to update the progress value in real-time. Here is a complete setup with a control button that simulates a file upload by incrementing the progress value every 400 milliseconds. The button is disabled during the simulation to prevent multiple triggers, and it updates the progress bar and percentage text in real-time.

This setup can be easily adapted to real file uploads by linking the progress value to the progress event of an XMLHttpRequest or using the Progress API.

The HTML

<div class="upload-widget">
  <progress id="js-progress" value="0" max="100"></progress>
  <div class="status-panel">
    <span id="percent-text">0%</span>
    <button id="trigger-upload">Simulate File Upload</button>
  </div>
</div>

The CSS

.upload-widget {
  max-width: 500px;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
}
#js-progress {
  width: 100%;
  height: 15px;
  margin-bottom: 10px;
}
.status-panel {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
#trigger-upload {
  cursor: pointer;
  padding: 8px 16px;
  border-radius: 4px;
  border: none;
  background: #007bff;
  color: white;
}

The JavaScript

document
  .getElementById("trigger-upload")
  .addEventListener("click", function () {
    const bar = document.getElementById("js-progress");
    const text = document.getElementById("percent-text");
    let val = 0;

    this.disabled = true; // Lock button during simulation

    const timer = setInterval(() => {
      val += Math.floor(Math.random() * 10);
      if (val >= 100) {
        val = 100;
        clearInterval(timer);
        this.innerText = "Success!";
        this.style.background = "#28a745";
      }
      bar.value = val;
      text.innerText = val + "%";
    }, 400);
  });

Essential Accessibility Best Practices for Progress Indicators

When we style or build custom components, it is remarkably easy to forget about users who rely on assistive technologies. Fortunately, the native <progress> element handles a lot of this for us. It has an implicit role of progressbar.

Include a descriptive label using either a <label> element or aria-label for better context along with the value, min, and max attributes, so screen readers will announce the progress correctly.

However, if you choose the custom <div> route, you must manually add the appropriate ARIA attributes according to the WAI-ARIA specifications:

  • role="progressbar": Identifies the element as a progress bar.
  • aria-valuenow: The current value.
  • aria-valuemin: The minimum value (usually zero).
  • aria-valuemax: The maximum value.
  • aria-label: Provides a descriptive name for the bar (e.g., “Upload Progress”).

Additionally, ensure that the contrast ratio between your bar color and the background is at least 3:1 to satisfy WCAG guidelines.

HTML Progress Bar vs Custom Div Progress Bar: Which is Better?

I often get asked which approach is better. The truth is, it depends on your project’s requirements.

FeatureNative <progress>Custom <div> Bar
SimplicityOne tag, very clean.Multiple nested elements.
AccessibilityAuto-handled by browsers.Requires manual ARIA maintenance.
Browser SupportUniversal.Universal.
Styling ControlMedium - limited by browser engines.High - full CSS control.
AnimationsCan be finicky.Very easy to animate.
ReliabilityConsistent behavior.High (consistent across all).
ComplexityVendor prefixes required.Simple CSS, no prefix mess.

If you just need a standard bar that works, stick with the native element. If you are building a highly branded or interactive application, the custom route is usually worth the extra ten minutes of work.

Conclusion

Mastering the HTML Progress Bar is a small but powerful step in leveling up your frontend development skills. By understanding how to navigate the complexities of browser defaults and vendor prefixes, you can create a seamless and visually stunning experience for your users.

Remember to keep Accessibility at the forefront of your design decisions. Whether you use the native tag or a custom implementation, the goal is always to provide clear, helpful feedback.

I hope this guide has been helpful! If you enjoyed this tutorial, you might also want to check out my article on building a loading spinner or how to use CSS variables like a pro to make your progress bars even more dynamic.

Useful Resources

Frequently Asked Questions

1. Can I use the HTML5 progress bar for file uploads? Yes! This is one of the most common and appropriate use cases. You can link the value of the progress bar to the progress event of your XMLHttpRequest or the Progress API.

2. How do I style a progress bar in HTML and CSS? Use the <progress> element, reset native styles with appearance: none, then style browser-specific pseudo-elements (::-webkit-progress-bar, ::-webkit-progress-value, and ::-moz-progress-bar) for full cross-browser control.

3. Why doesn’t the CSS transition property work on the progress bar’s value? Because the <progress> element is a form control, its internal rendering is controlled by the browser engine. For complex animations, the custom div approach is much more reliable and easier to animate.

4. What is the difference between <progress> and <meter>? Use <progress> when you want to show how much of a task has been completed (e.g., a download). Use <meter> for scalar measurements within a known range that aren’t necessarily “progress” toward a goal (e.g., disk usage, battery level, or a password strength meter).

5. How do I make the progress bar indeterminate? Simply remove the value attribute from the <progress> element. Browsers will automatically display an indeterminate animation.


If this article helped you, it might help someone else too. Click the share button below to spread the word!
Got thoughts or questions? Lets connect on X or LinkedIn.
Till next time, happy coding! 😊

Man waving goodbye

Enjoyed this article?

Join Bits & Notes to get new posts and monthly updates in your inbox.

More posts in web development

Support My Work

Choose your preferred platform