Loading Developer Playground

Loading ...

Skip to main content
ARIA ROLEWidget Roles

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.

Key Attributes
aria-valuenow, aria-valuemin, aria-valuemax
Native HTML Equivalent
<progress>
Keyboard Support
None (read-only)

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

Progress: 0%

Indeterminate Progress Bar

Status: Loading...

Circular Progress Indicator

0%

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-valuenow

Current progress value (omit for indeterminate)

aria-valueminRequired*

Minimum value (usually 0)

aria-valuemaxRequired*

Maximum value (e.g., 100 for percentage)

aria-valuetext

Human-readable text description of progress

aria-label

Accessible name for the progress bar

aria-labelledby

References element(s) providing the label

aria-describedby

References element(s) with additional description

aria-busy

Indicates the element is being updated

* aria-valuemin and aria-valuemax are required for determinate progress bars.

Common Use Cases

File upload progress
Download progress indicators
Multi-step form wizards
Data loading spinners
Profile/account completion
Course/lesson progress
Installation progress
Video/audio buffering
Skill level indicators
Reading progress (scroll)

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.

Related Roles & Attributes

Specifications & Resources