How to Create a Progress Bar in CSS
Build a progress bar that visually communicates how far along a task or value is, using either the native <progress> element or a styled div.
What a progress bar communicates
A progress bar turns a number into a visual proportion: a file upload at 40%, a multi-step form on step 3 of 5, a skill level out of 100. There are two ways to build one on the web: the native `<progress>` element, which is semantic and accessible for free, and a div-based bar, which gives you full control over colors, stripes, and animation.
Option 1: the native <progress> element
`<progress>` takes a `value` and a `max` attribute and the browser renders the fill automatically. Because it's a real form-associated element, screen readers announce it as a progress indicator with its current percentage - something a plain div can never do without extra ARIA attributes.
Example: styled native <progress>
<!DOCTYPE html>
<html>
<head>
<style>
progress {
width: 100%;
height: 18px;
border-radius: 9px;
overflow: hidden; /* needed so the rounded corners clip the fill in Firefox */
border: none;
}
/* Chrome/Edge/Safari track and fill */
progress::-webkit-progress-bar {
background: #e6e8eb;
}
progress::-webkit-progress-value {
background: #2f7d5b;
transition: width 0.2s ease;
}
/* Firefox track and fill */
progress::-moz-progress-bar {
background: #2f7d5b;
}
</style>
</head>
<body>
<progress value="40" max="100"></progress>
</body>
</html>Option 2: a div-based bar for full control
When you need gradients, striped animation, or a percentage label drawn inside the bar, build it from two nested divs: an outer `.progress-track` that acts as the background, and an inner `.progress-fill` whose `width` is set to the percentage - either directly in CSS or dynamically with JavaScript.
Example: div-based progress bar updated with JavaScript
<!DOCTYPE html>
<html>
<head>
<style>
.progress-track {
width: 100%;
height: 18px;
background: #e6e8eb;
border-radius: 9px;
overflow: hidden;
}
.progress-fill {
height: 100%;
width: 0%; /* JavaScript sets the real value */
background: linear-gradient(90deg, #3b82f6, #2563eb);
border-radius: 9px;
transition: width 0.3s ease;
text-align: right;
color: #fff;
font-size: 0.7rem;
line-height: 18px;
padding-right: 6px;
box-sizing: border-box;
white-space: nowrap;
}
</style>
</head>
<body>
<div class="progress-track">
<div class="progress-fill" id="fill">0%</div>
</div>
<button id="load-btn">Simulate loading</button>
<script>
var fill = document.getElementById('fill');
var button = document.getElementById('load-btn');
button.addEventListener('click', function () {
var percent = 0;
var timer = setInterval(function () {
percent += 10;
fill.style.width = percent + '%';
fill.textContent = percent + '%';
if (percent >= 100) clearInterval(timer);
}, 200);
});
</script>
</body>
</html>Setting `fill.style.width` to a percentage string is what makes the bar grow - the CSS `transition: width` line then animates that change smoothly instead of jumping instantly to the new width.
- Use <progress> when the value is a genuine, known measurement (like file upload percentage).
- Use a div-based bar when you need custom colors, stripes, or an inline label.
- Whichever you choose, keep the numeric value visible somewhere in text too, for users who cannot rely on color alone.
- If you build a div-based bar, add role="progressbar" plus aria-valuenow, aria-valuemin, and aria-valuemax attributes on the track so assistive technology can announce it the same way it would announce a native <progress> element.
Frequently Asked Questions
- How do I create a progress bar in CSS?
- Nest a fill div inside a track div. Give the track a background and a fixed height, then set the fill's width as a percentage. Add a transition on width so changes animate rather than jumping.
- Should I use the HTML progress element instead?
- Use
<progress>when you are reporting real task completion, since it carries the right semantics for screen readers. Build your own from divs when you need full styling control, as<progress>is awkward to style consistently across browsers.