progressbar
An element that displays the progress status for tasks that take a long time. Progress bars communicate to users how much of a task has been completed and how much remains.
Overview
The progressbar role identifies an element that displays the progress status for a task that takes a long time or consists of multiple steps. It provides essential feedback to users about ongoing processes like file uploads, form submissions, or data loading.
Progress bars can be determinate (showing a specific percentage complete) or indeterminate (indicating that work is in progress without specifying how much remains). The choice between these depends on whether you can calculate the completion percentage.
Native <progress> vs role="progressbar"
The native HTML <progress> element provides built-in semantics and is well-supported by assistive technologies. Use role="progressbar" only when you need custom styling that cannot be achieved with the native element or when building complex progress indicators like circular progress rings.
Live Demo: Progress Bar Types
Determinate Progress Bar
Indeterminate Progress Bar
Circular Progress Indicator
Circular progress indicators work great for profile completion, skill levels, or any percentage-based visualization.
Screen reader behavior: When progress changes, screen readers can announce the new value. Use aria-live regions to control announcement frequency and avoid overwhelming users.
Code Examples
Basic Progress Bar
<!-- Basic Progress Bar -->
<div
role="progressbar"
aria-valuenow="50"
aria-valuemin="0"
aria-valuemax="100"
aria-label="File upload progress"
>
<div class="progress-fill" style="width: 50%"></div>
</div>
<style>
[role="progressbar"] {
width: 100%;
height: 20px;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4f46e5, #7c3aed);
transition: width 0.3s ease;
}
</style>Native HTML (Preferred)
<!-- Native HTML Progress Element (Preferred) -->
<label for="file-progress">File upload progress</label>
<progress id="file-progress" value="50" max="100">50%</progress>
<!--
Benefits of native <progress>:
- Built-in accessibility semantics
- Automatic screen reader support
- Works without JavaScript
- Browser-styled by default
-->Indeterminate Progress
<!-- Indeterminate Progress Bar (unknown completion) -->
<div
role="progressbar"
aria-label="Loading content"
aria-valuetext="Loading..."
>
<div class="indeterminate-animation"></div>
</div>
<!-- Note: Omit aria-valuenow for indeterminate state -->
<style>
.indeterminate-animation {
width: 30%;
height: 100%;
background: linear-gradient(90deg, #4f46e5, #7c3aed);
animation: indeterminate 1.5s infinite ease-in-out;
}
@keyframes indeterminate {
0% { transform: translateX(-100%); }
100% { transform: translateX(400%); }
}
</style>Using aria-valuetext
<!-- Progress with aria-valuetext for better descriptions -->
<div
role="progressbar"
aria-valuenow="3"
aria-valuemin="1"
aria-valuemax="5"
aria-valuetext="Step 3 of 5: Payment Information"
aria-label="Checkout progress"
>
<span class="sr-only">Step 3 of 5: Payment Information</span>
<!-- Visual indicators for each step -->
</div>
<!-- aria-valuetext provides meaningful context beyond just numbers -->
<!-- For file downloads, use descriptive text -->
<div
role="progressbar"
aria-valuenow="75"
aria-valuemin="0"
aria-valuemax="100"
aria-valuetext="75 percent complete, 1.5 MB of 2 MB downloaded"
aria-label="Download progress"
>
75% (1.5 MB / 2 MB)
</div>Live Region Announcements
<!-- Progress with live region for announcements -->
<div aria-live="polite" aria-atomic="true" class="sr-only">
<span id="progress-announcement"></span>
</div>
<div
role="progressbar"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
aria-label="Upload progress"
id="upload-progress"
>
<div class="progress-fill"></div>
</div>
<script>
const progressbar = document.getElementById('upload-progress');
const announcement = document.getElementById('progress-announcement');
let lastAnnounced = 0;
function updateProgress(value) {
progressbar.setAttribute('aria-valuenow', value);
progressbar.querySelector('.progress-fill').style.width = value + '%';
// Announce at meaningful intervals (every 25%)
if (value >= lastAnnounced + 25 || value === 100) {
lastAnnounced = Math.floor(value / 25) * 25;
if (value === 100) {
announcement.textContent = 'Upload complete';
} else {
announcement.textContent = value + ' percent uploaded';
}
}
}
</script>React Component
// React Progress Bar Component
import { useState, useEffect } from 'react';
interface ProgressBarProps {
value?: number; // undefined = indeterminate
max?: number;
label: string;
showPercentage?: boolean;
valueText?: string;
onComplete?: () => void;
}
function ProgressBar({
value,
max = 100,
label,
showPercentage = true,
valueText,
onComplete,
}: ProgressBarProps) {
const [announced, setAnnounced] = useState('');
const percentage = value !== undefined ? Math.round((value / max) * 100) : null;
const isIndeterminate = value === undefined;
// Announce progress at intervals
useEffect(() => {
if (percentage === null) return;
const milestones = [25, 50, 75, 100];
const currentMilestone = milestones.find(m => percentage >= m && percentage < m + 25);
if (percentage === 100) {
setAnnounced('Complete');
onComplete?.();
} else if (currentMilestone && percentage === currentMilestone) {
setAnnounced(`${currentMilestone} percent`);
}
}, [percentage, onComplete]);
return (
<div className="progress-container">
{/* Live region for announcements */}
<div className="sr-only" aria-live="polite" aria-atomic="true">
{announced && `${label}: ${announced}`}
</div>
<div className="progress-label">
<span>{label}</span>
{showPercentage && percentage !== null && (
<span>{percentage}%</span>
)}
</div>
<div
role="progressbar"
aria-valuenow={isIndeterminate ? undefined : value}
aria-valuemin={0}
aria-valuemax={max}
aria-valuetext={valueText || (percentage !== null ? `${percentage}%` : 'Loading')}
aria-label={label}
className={`progressbar ${isIndeterminate ? 'indeterminate' : ''}`}
>
<div
className="progress-fill"
style={{ width: isIndeterminate ? '30%' : `${percentage}%` }}
/>
</div>
</div>
);
}
// Usage Examples
function App() {
const [uploadProgress, setUploadProgress] = useState(0);
return (
<>
{/* Determinate progress */}
<ProgressBar
value={uploadProgress}
max={100}
label="Uploading files"
valueText={`${uploadProgress}% uploaded, 3 of 5 files`}
/>
{/* Indeterminate progress */}
<ProgressBar
label="Loading data"
showPercentage={false}
/>
{/* Step progress */}
<ProgressBar
value={3}
max={5}
label="Checkout progress"
valueText="Step 3 of 5: Payment"
showPercentage={false}
/>
</>
);
}Circular Progress Indicator
<!-- Circular/Ring Progress Indicator -->
<svg
role="progressbar"
aria-valuenow="75"
aria-valuemin="0"
aria-valuemax="100"
aria-label="Profile completion"
viewBox="0 0 100 100"
width="120"
height="120"
>
<!-- Background circle -->
<circle
cx="50" cy="50" r="45"
fill="none"
stroke="#e0e0e0"
stroke-width="8"
/>
<!-- Progress circle -->
<circle
cx="50" cy="50" r="45"
fill="none"
stroke="#4f46e5"
stroke-width="8"
stroke-linecap="round"
stroke-dasharray="282.7"
stroke-dashoffset="70.7"
transform="rotate(-90 50 50)"
/>
<!-- Percentage text -->
<text
x="50" y="50"
text-anchor="middle"
dominant-baseline="middle"
font-size="24"
font-weight="bold"
fill="currentColor"
>
75%
</text>
</svg>
<!-- stroke-dashoffset calculation:
circumference = 2 * π * radius = 2 * 3.14159 * 45 ≈ 282.7
offset = circumference * (1 - progress/100)
for 75%: 282.7 * (1 - 0.75) = 70.7 -->Progress Bar Types
Determinate
Shows exact progress when you know the total amount of work. Use aria-valuenow, aria-valuemin, and aria-valuemax.
Use for: File uploads, form step wizards, download progress, data processing with known item count.
Indeterminate
Shows that work is in progress without indicating completion percentage. Omit aria-valuenow and use aria-valuetext for status.
Use for: Initial data loading, network requests with unknown duration, background processes.
Best Practices
Use native <progress> element when possible for better accessibility support
Always provide aria-label or aria-labelledby to identify what task is progressing
Use aria-valuetext to provide meaningful descriptions beyond percentages
Announce progress at meaningful intervals (e.g., every 25%) using live regions
Announce completion clearly when progress reaches 100%
For indeterminate progress, omit aria-valuenow and set aria-valuetext="Loading"
Don't update aria-valuenow too frequently as it may overwhelm screen readers
Don't use progress bars for static values - they imply ongoing activity
Don't rely solely on visual animation to communicate progress
Don't forget to set aria-valuemin and aria-valuemax for determinate progress
Supported ARIA Attributes
aria-valuenowCurrent progress value (omit for indeterminate)
aria-valueminRequired*Minimum value (usually 0)
aria-valuemaxRequired*Maximum value (e.g., 100 for percentage)
aria-valuetextHuman-readable text description of progress
aria-labelAccessible name for the progress bar
aria-labelledbyReferences element(s) providing the label
aria-describedbyReferences element(s) with additional description
aria-busyIndicates the element is being updated
* aria-valuemin and aria-valuemax are required for determinate progress bars.
Common Use Cases
Accessibility Notes
Announcement Strategy
Progress bars are read-only widgets, so screen readers only announce changes when configured to do so. Use an aria-live region to announce progress at meaningful intervals (every 25%, at completion, or when errors occur). Avoid announcing every small increment as this can be overwhelming.
Visual Design Considerations
Ensure progress indicators have sufficient color contrast (3:1 minimum for graphical elements). Don't rely solely on color to show progress—include text percentage or completion status. For animations, respect prefers-reduced-motion media query for users who are sensitive to motion.
Screen Reader Behavior
Most screen readers announce progress bars as "[label], progress bar, [value]%". When using aria-valuetext, screen readers will use that text instead of the numeric value, making announcements like "Uploading files, progress bar, Step 3 of 5" possible.
Time and Patience
For long-running tasks, consider providing estimated time remaining in aria-valuetext (e.g., "50% complete, approximately 2 minutes remaining"). This gives users a better sense of how long they need to wait and whether they should stay on the page.

