HTML <progress> and <meter> reference
Jump to this section
Common attributes Jump to this section
Basic structures Jump to this section
HTML provides two elements for displaying measurements: <progress> for tasks in progress (like file uploads or loading), and <meter> for scalar measurements within a known range (like disk usage or ratings).
Use <progress> for things that are actively changing. Use <meter> for measurements that exist at a point in time.
<!-- Progress: for tasks in progress -->
<progress value="70" max="100"></progress>
<!-- Meter: for measurements at a point in time -->
<meter value="70" min="0" max="100"></meter>Progress attributes Jump to this section
value Jump to this section
Current value of the progress. Must be between 0 and max. If omitted, the progress bar is indeterminate (shows an animation).
Use value when you know the progress percentage. Omit it for loading states where the progress is unknown.
<!-- Determinate: known progress -->
<progress value="50" max="100"></progress>
<!-- Indeterminate: unknown progress -->
<progress></progress>max Jump to this section
Maximum value for the progress bar. Defaults to 1.0 if not specified. The value attribute is relative to this maximum.
Use values that match your use case: 100 for percentages, file size for uploads, or number of steps for multi-step processes.
<!-- Step progress -->
<progress value="7" max="10"></progress>
<!-- Decimal progress -->
<progress value="0.75" max="1"></progress>
<!-- File upload (bytes) -->
<progress value="2048576" max="5242880"></progress>Meter attributes Jump to this section
value Jump to this section
Current value being measured. Required attribute. Should be between min and max.
<meter value="6" min="0" max="10"></meter>
min Jump to this section
Minimum value of the range. Defaults to 0 if not specified.
Use when your measurement doesn't start at zero (like temperature scales or custom ranges).
<meter value="40" min="0" max="100"></meter>
<meter value="5" min="1" max="10"></meter>max Jump to this section
Maximum value of the range. Defaults to 1.0 if not specified.
<meter value="75" min="0" max="100"></meter>
low Jump to this section
The upper boundary of the "low" range. Values at or below this are considered low. Used for semantic coloring.
When value is between min and low, many browsers color the meter yellow/orange to indicate a low reading.
<meter value="15" min="0" max="100"
low="20" high="80"></meter>high Jump to this section
The lower boundary of the "high" range. Values at or above this are considered high.
When value is between high and max, browsers may use different coloring to indicate a high reading.
<meter value="92" min="0" max="100"
low="20" high="80"></meter>optimum Jump to this section
The optimal value within the range. Helps browsers determine the meter's semantic meaning and coloring.
- If
optimumis betweenminandlow: lower values are better (green when low, red when high) - If
optimumis betweenlowandhigh: middle values are better (green in middle, yellow/red at extremes) - If
optimumis betweenhighandmax: higher values are better (green when high, red when low)
<!-- Higher is better (battery) -->
<meter value="80" min="0" max="100"
low="20" high="80" optimum="100"></meter>
<!-- Middle is better (temperature) -->
<meter value="22" min="0" max="40"
low="15" high="25" optimum="20"></meter>Common use cases Jump to this section
Progress use cases Jump to this section
<!-- File upload progress -->
<label>Uploading file: <span id="percent">0%</span></label>
<progress id="upload" value="0" max="100"></progress>
<!-- Multi-step form -->
<label>Step 3 of 5</label>
<progress value="3" max="5"></progress>
<!-- Loading indicator (indeterminate) -->
<label>Loading...</label>
<progress></progress>
<!-- Download progress -->
<label>Downloading: 2.5 MB of 10 MB</label>
<progress value="2.5" max="10"></progress>
Meter use cases Jump to this section
<!-- Disk usage -->
<label>Disk usage: 75 GB of 100 GB</label>
<meter value="75" min="0" max="100" low="50" high="80"></meter>
<!-- Battery level -->
<label>Battery: 85%</label>
<meter value="85" min="0" max="100" low="20" high="80" optimum="100"></meter>
<!-- Rating score -->
<label>Review rating: 4.2 out of 5</label>
<meter value="4.2" min="0" max="5" low="2" high="4" optimum="5"></meter>
<!-- Temperature -->
<label>CPU Temperature: 65°C</label>
<meter value="65" min="0" max="100" low="40" high="80" optimum="50"></meter>
<!-- Relevance/similarity score -->
<label>Match: 78%</label>
<meter value="0.78" max="1" low="0.3" high="0.7" optimum="1"></meter>
Accessibility attributes Jump to this section
aria-label Jump to this section
Provides an accessible label when a visible <label> element isn't present.
<progress value="60" max="100" aria-label="Upload progress: 60%"></progress>
<meter value="75" max="100" aria-label="Disk usage: 75%"></meter>
aria-labelledby Jump to this section
References a label element by its id.
<span id="upload-label">File upload progress</span>
<progress value="45" max="100" aria-labelledby="upload-label"></progress>
aria-describedby Jump to this section
References descriptive text to provide additional context.
<progress value="50" max="100" aria-describedby="upload-desc"></progress>
<p id="upload-desc">Uploading profile picture...</p>
aria-valuenow, aria-valuemin, aria-valuemax Jump to this section
These are automatically set by browsers for <progress> and <meter>, but can be explicitly set if needed.
<progress value="50" max="100"
aria-valuenow="50"
aria-valuemin="0"
aria-valuemax="100"></progress>
role Jump to this section
Both elements have implicit roles (progressbar for progress, meter for meter). Generally don't need to be overridden.
<progress role="progressbar" value="50" max="100"></progress>
<meter role="meter" value="50" max="100"></meter>
Common attributes Jump to this section
id Jump to this section
Unique identifier for the element. Essential for JavaScript manipulation and label association.
<label for="file-progress">Upload progress:</label>
<progress id="file-progress" value="0" max="100"></progress>
<script>
const progress = document.getElementById('file-progress');
progress.value = 75;
</script>
class Jump to this section
Assigns CSS class names for styling.
<progress class="progress-large progress-primary" value="60" max="100"></progress>
<meter class="meter-success" value="80" max="100"></meter>
data-* attributes Jump to this section
Custom attributes for storing application-specific data.
<progress value="50" max="100"
data-upload-id="abc123"
data-file-name="document.pdf"></progress>
<meter value="75" max="100"
data-metric="disk-usage"
data-threshold="warning"></meter>
<script>
const progress = document.querySelector('progress');
console.log(progress.dataset.uploadId); // "abc123"
console.log(progress.dataset.fileName); // "document.pdf"
</script>
title Jump to this section
Provides tooltip text on hover. Not accessible to keyboard-only users or touch devices.
<progress value="75" max="100" title="Upload is 75% complete"></progress>
<meter value="65" max="100" title="Battery level: 65%"></meter>
Fallback content Jump to this section
Both elements support fallback content for browsers that don't support them (rare nowadays). The content between tags is only shown in unsupported browsers.
<progress value="70" max="100">
<span>70%</span>
</progress>
<meter value="6" min="0" max="10">
6 out of 10
</meter>
Styling basics Jump to this section
Progress and meter elements are notoriously difficult to style consistently across browsers. Each browser uses different pseudo-elements for styling, and appearance varies significantly.
Basic styling Jump to this section
progress, meter {
width: 250px;
height: 20px;
}
/* Note: Default styling varies by browser */Reset default appearance Jump to this section
Remove browser-specific styling to apply custom styles.
progress {
appearance: none;
width: 250px;
height: 12px;
border: none;
background: #e5e7eb;
border-radius: 10px;
overflow: hidden;
}
/* WebKit (Chrome, Safari, Edge) */
progress::-webkit-progress-bar {
background: #e5e7eb;
border-radius: 10px;
}
progress::-webkit-progress-value {
background: #3b82f6;
border-radius: 10px;
}
/* Firefox */
progress::-moz-progress-bar {
background: #3b82f6;
border-radius: 10px;
}Progress bar styles Jump to this section
Modern rounded progress Jump to this section
.progress-modern {
appearance: none;
width: 250px;
height: 8px;
border: none;
background: #f3f4f6;
border-radius: 10px;
}
.progress-modern::-webkit-progress-bar {
background: #f3f4f6;
border-radius: 10px;
}
.progress-modern::-webkit-progress-value {
background: #3b82f6;
border-radius: 10px;
}
.progress-modern::-moz-progress-bar {
background: #3b82f6;
border-radius: 10px;
}Thick progress bar Jump to this section
.progress-thick {
appearance: none;
width: 250px;
height: 24px;
border: none;
background: #e5e7eb;
border-radius: 12px;
}
.progress-thick::-webkit-progress-value {
background: linear-gradient(90deg, #3b82f6, #2563eb);
border-radius: 12px;
}
.progress-thick::-moz-progress-bar {
background: linear-gradient(90deg, #3b82f6, #2563eb);
border-radius: 12px;
}Striped progress Jump to this section
.progress-striped::-webkit-progress-value {
background: linear-gradient(
45deg,
rgba(255,255,255,.15) 25%,
transparent 25%,
transparent 50%,
rgba(255,255,255,.15) 50%,
rgba(255,255,255,.15) 75%,
transparent 75%,
transparent
), #3b82f6;
background-size: 20px 20px;
border-radius: 10px;
}Gradient progress Jump to this section
.progress-gradient::-webkit-progress-value {
background: linear-gradient(
90deg,
#06b6d4,
#3b82f6,
#8b5cf6
);
border-radius: 8px;
}Progress with shadow Jump to this section
.progress-shadow {
appearance: none;
background: #f3f4f6;
border-radius: 10px;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
}
.progress-shadow::-webkit-progress-value {
background: #10b981;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(16, 185, 129, 0.4);
}Color variants Jump to this section
.progress-success::-webkit-progress-value {
background: #10b981; /* Green */
}
.progress-warning::-webkit-progress-value {
background: #f59e0b; /* Orange */
}
.progress-danger::-webkit-progress-value {
background: #ef4444; /* Red */
}Meter styles Jump to this section
Custom meter styling Jump to this section
.meter-custom {
appearance: none;
width: 250px;
height: 16px;
background: #e5e7eb;
border-radius: 8px;
}
.meter-custom::-webkit-meter-optimum-value {
background: #10b981; /* Green - good */
border-radius: 8px;
}
.meter-custom::-webkit-meter-suboptimum-value {
background: #f59e0b; /* Orange - warning */
border-radius: 8px;
}
.meter-custom::-webkit-meter-even-less-good-value {
background: #ef4444; /* Red - bad */
border-radius: 8px;
}Meter with border Jump to this section
.meter-border {
appearance: none;
width: 250px;
height: 20px;
background: white;
border: 2px solid #d1d5db;
border-radius: 10px;
}Circular progress (CSS only) Jump to this section
.circular-progress {
width: 80px;
height: 80px;
border-radius: 50%;
background: conic-gradient(
#3b82f6 0% 65%,
#e5e7eb 65% 100%
);
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.circular-progress::before {
content: '';
width: 60px;
height: 60px;
border-radius: 50%;
background: white;
position: absolute;
}
.circular-progress-text {
position: relative;
z-index: 1;
font-weight: 600;
}Indeterminate animation Jump to this section
@keyframes indeterminate {
0% { transform: translateX(-100%); }
100% { transform: translateX(400%); }
}
.progress-indeterminate::-webkit-progress-value {
background: #3b82f6;
animation: indeterminate 1.5s infinite ease-in-out;
}With label overlay Jump to this section
.progress-container {
position: relative;
}
.progress-label {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 13px;
font-weight: 600;
color: white;
mix-blend-mode: difference;
}JavaScript patterns Jump to this section
Progress and meter elements are commonly manipulated with JavaScript to reflect real-time updates, track completion, and provide visual feedback.
Update progress value Jump to this section
Change the progress value dynamically.
const progress = document.getElementById('my-progress');
// Set value directly
progress.value = 50;
// Increment
progress.value += 10;
// Decrement
progress.value -= 10;
// Reset
progress.value = 0;
// Set to maximum
progress.value = progress.max;Animate progress Jump to this section
Smoothly animate progress from one value to another.
function animateProgress(elementId, targetValue, duration) {
const progress = document.getElementById(elementId);
const startValue = progress.value;
const difference = targetValue - startValue;
const startTime = performance.now();
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress_ratio = Math.min(elapsed / duration, 1);
progress.value = startValue + (difference * progress_ratio);
if (progress_ratio < 1) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
}
// Usage
animateProgress('my-progress', 75, 1000); // To 75% in 1 secondCalculate percentage Jump to this section
Calculate and display the percentage from progress value.
const progress = document.getElementById('my-progress');
const display = document.getElementById('percent-display');
function updatePercentage() {
const percentage = (progress.value / progress.max) * 100;
display.textContent = Math.round(percentage) + '%';
}
// Update on value change
progress.value = 67;
updatePercentage(); // "67%"Simulate file upload Jump to this section
Create a realistic file upload progress simulation.
function simulateUpload() {
const progress = document.getElementById('upload-progress');
const status = document.getElementById('upload-status');
progress.value = 0;
status.textContent = 'Uploading...';
const interval = setInterval(() => {
progress.value += Math.random() * 10;
if (progress.value >= 100) {
progress.value = 100;
clearInterval(interval);
status.textContent = 'Upload complete!';
status.style.color = '#10b981';
} else {
const percent = Math.round(progress.value);
status.textContent = `Uploading... ${percent}%`;
}
}, 200);
}
// With fetch API (real upload)
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
progress.value = percent;
}
});
xhr.open('POST', '/upload');
xhr.send(formData);
}Multi-step progress Jump to this section
Track progress through multiple steps.
const progress = document.getElementById('step-progress');
const label = document.getElementById('step-label');
let currentStep = 1;
const totalSteps = 4;
function updateStep(direction) {
currentStep = Math.max(1, Math.min(totalSteps, currentStep + direction));
progress.value = currentStep;
label.textContent = `Step ${currentStep} of ${totalSteps}`;
// Optional: Show different content per step
showStepContent(currentStep);
}
function showStepContent(step) {
// Hide all steps
document.querySelectorAll('.step-content').forEach(el => {
el.style.display = 'none';
});
// Show current step
document.getElementById(`step-${step}`).style.display = 'block';
}Update meter dynamically Jump to this section
Change meter values and thresholds dynamically.
const meter = document.getElementById('disk-usage');
const display = document.getElementById('usage-display');
// Update value
meter.value = 75;
display.textContent = `${meter.value} GB of ${meter.max} GB used`;
// Update thresholds
meter.low = 40;
meter.high = 80;
meter.optimum = 20;
// Dynamic updates (e.g., from API)
async function updateDiskUsage() {
const response = await fetch('/api/disk-usage');
const data = await response.json();
meter.value = data.used;
meter.max = data.total;
display.textContent = `${data.used} GB of ${data.total} GB used`;
}Monitor progress completion Jump to this section
Detect when progress reaches 100% and trigger actions.
const progress = document.getElementById('complete-progress');
const status = document.getElementById('complete-status');
function startProgress() {
progress.value = 0;
status.textContent = 'In progress...';
status.style.color = '#6b7280';
const interval = setInterval(() => {
progress.value += 5;
if (progress.value >= 100) {
clearInterval(interval);
onComplete();
}
}, 100);
}
function onComplete() {
status.textContent = '✓ Complete!';
status.style.color = '#10b981';
// Trigger additional actions
showSuccessMessage();
enableNextButton();
logCompletion();
}Get progress percentage Jump to this section
Read the current progress as a percentage.
const progress = document.getElementById('my-progress');
// Calculate percentage
const percentage = (progress.value / progress.max) * 100;
console.log(`Progress: ${percentage}%`);
// Check if complete
if (percentage >= 100) {
console.log('Task completed!');
}
// Check if half complete
if (percentage >= 50) {
console.log('Halfway there!');
}
// Get remaining percentage
const remaining = 100 - percentage;
console.log(`${remaining}% remaining`);Progress with time estimate Jump to this section
Calculate and display estimated time remaining.
function startTimedProgress() {
const progress = document.getElementById('time-progress');
const estimate = document.getElementById('time-estimate');
const startTime = Date.now();
let lastUpdate = startTime;
const interval = setInterval(() => {
progress.value += 2;
const now = Date.now();
// Calculate time remaining
const elapsed = (now - startTime) / 1000; // seconds
const rate = progress.value / elapsed; // units per second
const remaining = progress.max - progress.value;
const timeRemaining = remaining / rate;
estimate.textContent = `${Math.round(timeRemaining)}s remaining`;
if (progress.value >= 100) {
clearInterval(interval);
estimate.textContent = 'Complete!';
}
}, 200);
}Pause and resume progress Jump to this section
Control progress execution with pause/resume functionality.
let progressInterval = null;
let isPaused = true;
function togglePause() {
const progress = document.getElementById('pause-progress');
const btn = document.getElementById('pause-btn');
if (isPaused) {
// Start/Resume
progressInterval = setInterval(() => {
progress.value = Math.min(100, progress.value + 2);
if (progress.value >= 100) {
clearInterval(progressInterval);
btn.textContent = 'Start';
isPaused = true;
}
}, 100);
btn.textContent = 'Pause';
isPaused = false;
} else {
// Pause
clearInterval(progressInterval);
btn.textContent = 'Resume';
isPaused = true;
}
}
function resetProgress() {
clearInterval(progressInterval);
document.getElementById('pause-progress').value = 0;
document.getElementById('pause-btn').textContent = 'Start';
isPaused = true;
}Multiple progress bars Jump to this section
Manage and update multiple progress bars simultaneously.
const progressBars = [
{ id: 'download1', rate: 5 },
{ id: 'download2', rate: 3 },
{ id: 'download3', rate: 7 }
];
function updateAllProgress() {
progressBars.forEach(bar => {
const element = document.getElementById(bar.id);
element.value += bar.rate;
if (element.value >= element.max) {
element.value = element.max;
}
});
}
// Update every 200ms
let updateInterval = setInterval(updateAllProgress, 200);
// Check if all complete
function allComplete() {
return progressBars.every(bar => {
const element = document.getElementById(bar.id);
return element.value >= element.max;
});
}