HTML Progress Bar, Meter, Time & Data: Complete Guide
An html progress bar is the browser’s built-in way to show how far through a task something is — a file upload, a multi-step form, a page loading its content. It needs no JavaScript, no nested divs, and no CSS framework: one <progress> element draws a bar that fills up as the work completes, and a screen reader can announce its value without any extra code from you. Most developers reach for a div and a width percentage out of habit, and end up rebuilding — badly — something the browser already ships.
The <progress> element is only one of four small semantic elements that most HTML guides skip over. Its twin, <meter>, looks almost identical but means something different: a measurement on a scale, like disk usage or a battery level, not a task moving toward completion. <time> marks up dates and times so machines can read them, and <data> pairs a human-readable label with a machine-readable value, like a product name with its SKU. All four are single-purpose, all four work without JavaScript, and all four give browsers, search engines, and assistive technology information that a plain div or span never can.
This guide covers each element properly: what it means, its attributes, complete working examples you can copy and run, how to style the two bar elements, the accessibility rules that apply to all four, and the mistakes that come from confusing them — especially the classic one, using <progress> for a gauge and <meter> for a progress bar.
Quick Takeaways
- An html progress bar (
<progress>) shows task completion moving from zero toward a maximum.<meter>shows a measurement inside a known range. They look alike and are not interchangeable. - A
<progress>element with novalueattribute renders an indeterminate bar — something is happening, but the length is unknown. Use it only when the length really is unknown. <meter>has three extra attributes —low,high, andoptimum— that divide the scale into zones. The browser uses them to colour the bar, but the exact colours vary by browser.<time datetime="2026-11-14">14 November 2026</time>lets search engines, calendars, and browser extensions read a date exactly, whatever format the human-readable text uses.<data value="SKU-4812">Wireless Mouse</data>pairs a human label with a machine-readable value. It is not the same thing as adata-*attribute.- Always label these elements and include fallback text between the tags. A screen reader announces the value, not the picture — and the fallback text is what older browsers show.
1. Four Small Elements, Four Different Jobs
HTML has a set of small elements that each describe one specific kind of content. They take minutes to learn, they cost nothing to use, and they make pages clearer for every reader — human and machine. The four in this guide are the ones beginners most often replace with a div, a span, or plain text, losing meaning in the process.
| Element | What it shows | Example | Key attributes |
|---|---|---|---|
<progress> | Completion of a task | File upload 70% done | value, max |
<meter> | A measurement in a known range | Disk 80% full | value, min, max, low, high, optimum |
<time> | A date or time, machine-readable | 14 November 2026 | datetime |
<data> | A value with a machine-readable form | Product name + SKU | value |
The one-sentence rule that decides between the two bar elements: if the value moves toward finishing a task, it is progress; if it is a reading on a scale that can go up and down, it is a meter. A download moving from 0% to 100% is progress. Disk space sitting at 80% is a meter. A quiz score of 8 out of 10 is a meter, because the score is a measurement, not a task in progress — even though both render as a filled bar.
<time> and <data> are quieter elements. They do not draw anything special on the page: <time> renders as ordinary inline text, and <data> renders as its contents. Their value is invisible — it lives in the datetime and value attributes, where search engines, scripts, and assistive technology can read an exact, unambiguous value instead of guessing from human text. A date written as “14/11/2026” means 14 November in most of the world and 11 April in the United States; <time datetime="2026-11-14"> has no such ambiguity.
All four elements are part of the HTML Standard and work in every current browser. The two bar elements have been supported since the early 2010s — <progress> since Chrome 8, Firefox 16, Safari 6, Internet Explorer 10, and Opera 11, and <meter> since Chrome 8, Edge 13, Firefox 16, Safari 6, and Opera 11.5 (Internet Explorer never supported <meter>). There is no compatibility reason left to build either bar from divs.
2. The HTML Progress Bar: the <progress> Element in Depth
The <progress> element represents the completion progress of a task. That definition is deliberately narrow: a task, with a known amount of total work, that is partway done. File uploads, multi-step forms, exports, installations, and page loads are progress. Disk usage, battery level, and ratings are not — that is what <meter> is for, and Section 4 explains the difference in full.
2.1 value and max
Two attributes control the bar. max sets how much work the task requires in total, and value sets how much of it is complete. Both are floating-point numbers, so fractions are fine.
<label for="upload">Uploading report.pdf</label>
<progress id="upload" value="70" max="100">70%</progress>
Three rules govern the attributes. First, max, when present, must be greater than zero; if you leave it out, it defaults to 1, so value="0.7" with no max means 70%. Second, value must sit between zero and max — a value above max is invalid and the browser will clamp it. Third, there is no min attribute on <progress>: the minimum is always zero, and writing min="0" is a validation error. That is the first of several small differences from <meter>, which does have a min.
The text between the tags — 70% in the example — is fallback content. Modern browsers hide it and draw the bar instead, but it is what users of very old browsers see, and it is good practice to keep it accurate. It is not, however, an accessible label: text between the tags is not announced as the element’s name, which is why Section 2.3 covers labelling separately.
2.2 Indeterminate progress: when you do not know how long it takes
Leave the value attribute off entirely and the bar becomes indeterminate: it animates to show that something is happening, with no indication of how long it will take.
<label for="loading">Loading your dashboard</label>
<progress id="loading">Loading…</progress>
Use the indeterminate bar only when the length of the task is genuinely unknown — a server request with no progress events, a search with no total to count against. The moment you know a total, switch to a determinate bar with a real value. An indeterminate bar that runs for a known-length task is worse than no bar at all, because it tells the user nothing they can act on. You can match an indeterminate bar in CSS with the :indeterminate pseudo-class, and you can turn a determinate bar indeterminate from JavaScript by removing the attribute: element.removeAttribute('value').
2.3 Labels and fallback text
A bare <progress> element is not accessible on its own. Screen readers need a name for it, and the text between the tags does not provide one. Give it a name with a <label> element, as in the examples above, or with aria-label / aria-labelledby when no visible label fits the design.
<progress value="45" max="100" aria-label="Profile completion">45%</progress>
The <progress> element has an implicit ARIA role of progressbar, so a screen reader announces it as a progress bar together with its current value — “progress bar, 45 percent” — without any ARIA attributes from you. What it cannot do is invent a name, so the label is your job. Keep the fallback text between the tags in sync with the value as well: if JavaScript updates the bar, update the text too, so the small number of users on browsers without <progress> support still see the truth.
2.4 Updating a progress bar with JavaScript
A progress bar earns its keep when it moves. The usual pattern is simple: a script updates the value property as work completes, and updates the fallback text at the same time. This complete document simulates a file upload — click the button and watch the bar fill in steps, exactly as a real upload handler would drive it from upload-progress events.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HTML Progress Bar Demo</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 560px; margin: 2rem auto; padding: 0 1rem; }
progress { width: 100%; height: 1.5rem; }
button { padding: 0.6rem 1.2rem; font-size: 1rem; margin-top: 1rem; }
</style>
</head>
<body>
<h1>Upload demo</h1>
<label for="file-progress">Uploading holiday-photos.zip</label>
<progress id="file-progress" value="0" max="100">0%</progress>
<p id="status">Ready to upload.</p>
<button id="start" type="button">Start upload</button>
<script>
const bar = document.getElementById('file-progress');
const status = document.getElementById('status');
const button = document.getElementById('start');
button.addEventListener('click', () => {
button.disabled = true;
let done = 0;
const timer = setInterval(() => {
done += 8;
if (done >= 100) {
done = 100;
clearInterval(timer);
status.textContent = 'Upload complete.';
button.disabled = false;
} else {
status.textContent = 'Uploading… ' + done + '%';
}
bar.value = done;
bar.textContent = done + '%';
}, 300);
});
</script>
</body>
</html>
Two details in that script are worth copying into real code. First, bar.value = done updates the attribute and the rendering together — there is no separate repaint step. Second, bar.textContent = done + '%' keeps the fallback text accurate, which is the part most tutorials forget.
2.5 Progress bars in forms
<progress> is a labelable element, which means a <label> can be attached to it exactly as it attaches to an input. It can also be associated with a form that sits elsewhere on the page, using the form attribute with the form’s id — useful when the bar lives in a page header while the form it reports on is further down.
<form id="signup-form" action="/signup" method="post">
<!-- form fields here -->
</form>
<label for="signup-progress">Sign-up progress</label>
<progress id="signup-progress" form="signup-form" value="2" max="4">Step 2 of 4</progress>
A multi-step form is the most common real-world use of <progress>: step 2 of 4, step 3 of 4, and so on, with the bar advancing as each step is completed. For the full picture of form markup, including labels and validation attributes, see the guide to mastering forms in HTML.
3. <meter>: A Gauge, Not a Progress Bar
The <meter> element represents a scalar measurement within a known range, or a fractional value — disk usage, the relevance of a search result, the fraction of voters who picked a candidate, a battery level, a quiz score. The value is a reading on a scale. It can go up and down, and it is never “finished”. That is the whole difference from <progress>, and it is the difference the HTML Standard itself spells out: the meter element should not be used to indicate progress, and the progress element is the wrong element for a gauge.
3.1 min, max, and value
<meter> takes the same value and max as <progress>, plus a min that <progress> does not allow. The defaults are min="0" and max="1", so a bare <meter value="0.8"> means 80% of the scale.
<label for="disk">Disk usage</label>
<meter id="disk" value="0.8" min="0" max="1">80% full</meter>
Unlike <progress>, the value attribute on <meter> is required — a meter with no value has nothing to measure. And as with <progress>, include a text version of the reading between the tags. The specification encourages it, and it is what users of browsers without <meter> support see instead of the bar.
3.2 low, high, and optimum: zones on the scale
Three extra attributes divide the scale into zones, and the browser uses them to decide which colour the bar gets. low marks the top of the low zone, high marks the bottom of the high zone, and optimum says which value is ideal — which tells the browser which zone counts as “good”.
<label for="battery">Battery</label>
<meter id="battery" value="0.25" min="0" max="1" low="0.2" high="0.8" optimum="1">25%</meter>
In that example the optimum is 1, so high values are good and a value of 0.25 sits in the low zone — the browser renders it in a warning colour. Flip the optimum to 0 and the same value becomes good: a golf score, a fuel-consumption figure, or a response time are all measurements where low is better, and optimum is how you say so. For a disk-usage meter, the optimum is low (empty disk) and the high zone is the danger area; for a battery, the optimum is high and the low zone is the danger area. The same three attributes describe both, with no extra markup.
Two honest caveats. First, the exact colours vary by browser — Chrome, Firefox, and Safari all colour the zones, but they do not agree on the shades, and they disagree slightly on whether a value sitting exactly on a boundary belongs to one zone or the next. Second, colour alone must never be the only signal: always show the number in text as well, so the meaning survives for users who cannot tell the colours apart. Section 8 covers this in the accessibility rules.
3.3 A complete example: a server status dashboard
Meters shine in dashboards, where several readings share one screen. This complete document builds a small server status page: disk usage, memory usage, and battery level, each with zones that match what “good” means for that measurement. Save it, open it, and change the value attributes to see the colours change.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Server Status Dashboard</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 560px; margin: 2rem auto; padding: 0 1rem; }
.reading { margin-bottom: 1.5rem; }
label { display: block; font-weight: 600; margin-bottom: 0.25rem; }
meter { width: 100%; height: 1.5rem; }
.value { color: #555; }
</style>
</head>
<body>
<h1>Server status</h1>
<div class="reading">
<label for="disk">Disk usage</label>
<meter id="disk" value="0.82" min="0" max="1" low="0.5" high="0.85" optimum="0">82% used</meter>
<span class="value">82% used — 164 GB of 200 GB</span>
</div>
<div class="reading">
<label for="memory">Memory usage</label>
<meter id="memory" value="0.61" min="0" max="1" low="0.5" high="0.85" optimum="0">61% used</meter>
<span class="value">61% used — 9.8 GB of 16 GB</span>
</div>
<div class="reading">
<label for="battery">UPS battery</label>
<meter id="battery" value="0.94" min="0" max="1" low="0.2" high="0.8" optimum="1">94% charged</meter>
<span class="value">94% charged — about 42 minutes of runtime</span>
</div>
</body>
</html>
Notice how the optimum flips between the readings: 0 for disk and memory (empty is good) and 1 for the battery (full is good). The low and high values mark where each reading stops being comfortable. That is the entire zone system — three attributes, no JavaScript, no CSS.
4. progress vs meter: How to Choose
If you remember one table from this guide, make it this one.
<progress> | <meter> | |
|---|---|---|
| Question it answers | How far through the task are we? | What is the reading on this scale? |
| Value moves toward | Completion (0 to max) | Nothing — it is a snapshot |
| Value can go down? | No, not normally | Yes, freely |
min attribute | Not allowed (always 0) | Allowed, default 0 |
low, high, optimum | Not allowed | The zone system |
No value attribute | Indeterminate bar | Invalid — value is required |
| Example | File upload at 70% | Disk 80% full |
| Screen reader announces | Progress bar, 70 percent | Meter, 80 percent |
| Wrong use | Disk usage, ratings, scores | File uploads, form steps |
The decision rule is one question: is the value moving toward finishing something? A file uploading, a form being completed step by step, an export running — yes, that is progress. A battery level, a storage reading, a test score, a star rating, a password-strength reading — no, those are measurements, and they belong in a meter. When in doubt, ask whether “100%” means “done” or “full”. Done means progress; full means meter.
The confusion is understandable because the two elements render almost identically in most browsers — a horizontal bar, partly filled. But the similarity is only visual. A screen reader announces them differently, a search engine understands them differently, and a user who hears “progress bar, 80 percent” forms a different expectation from one who hears “meter, 80 percent”: the first expects the task to finish, the second expects a reading that might stay at 80% all day. Using the wrong element tells the wrong story.
5. <time>: Dates and Times Machines Can Read
The <time> element represents a specific period of time: a date, a time of day, a date and time together, or a duration. The visible text can be written any way you like — “14 November 2026”, “Nov 14”, “tomorrow” — while the datetime attribute holds the same moment in a standard, machine-readable format that search engines, calendars, and browser extensions can parse exactly.
5.1 The datetime attribute
The datetime value follows the ISO 8601 formats. These are the forms you will actually use:
| What you mean | datetime value | Visible text can be |
|---|---|---|
| A date | 2026-11-14 | 14 November 2026 |
| A time | 19:30 | 7:30 in the evening |
| Date and time | 2026-11-14T19:30 | 14 November, 7:30 pm |
| With a time zone | 2026-11-14T19:30Z | 14 November, 7:30 pm UTC |
| With an offset | 2026-11-14T19:30+05:00 | 14 November, 7:30 pm PKT |
| A month | 2026-11 | November 2026 |
| A year | 2026 | 2026 |
The pattern is consistent: year-month-day, then a T, then hours and minutes, then an optional zone — Z for UTC or an offset like +05:00. Write the value exactly in that shape and any parser in the world will read it the same way, which is precisely what human text cannot promise: “14/11/2026” is 14 November in most countries and 11 April in the United States.
<p>Published on <time datetime="2026-11-14">14 November 2026</time>.</p>
<p>The webinar starts at <time datetime="2026-11-20T19:30+05:00">8:30 pm PKT on 20 November</time>.</p>
You may leave datetime off, but only when the text content itself is already in a machine-readable format — <time>2026-11-14</time> is valid, <time>14 November 2026</time> without a datetime attribute is not. The safe habit is to always write datetime and treat the visible text as purely human-facing.
5.2 Durations
A <time> element can also hold a duration, written as P followed by the parts: PT2H30M means 2 hours 30 minutes, PT45M means 45 minutes, P7D means 7 days. This is the form recipe sites, video pages, and event listings use to tell search engines how long something lasts.
<p>Reading time: <time datetime="PT12M">12 minutes</time></p>
<p>Video length: <time datetime="PT2H30M">2 hours 30 minutes</time></p>
Search engines use durations from <time> elements (and from structured data) to show “12 min read” style labels in results, so a reading-time or video-length mark is one of the highest-value places to use the element.
5.3 A complete example: an article header with dates
The most common real use of <time> is article metadata: published date, updated date, and event dates. This complete document shows all three, including a duration for reading time, and it is a pattern you can lift straight into a blog template.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Article Dates with the time Element</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 560px; margin: 2rem auto; padding: 0 1rem; }
.meta { color: #555; }
</style>
</head>
<body>
<article>
<h1>How to Water a Monstera</h1>
<p class="meta">
Published <time datetime="2026-11-14">14 November 2026</time>
· Updated <time datetime="2026-11-20">20 November 2026</time>
· Reading time <time datetime="PT8M">8 minutes</time>
</p>
<p>Monsteras like their soil to dry out between waterings…</p>
<h2>Next workshop</h2>
<p>Join us on <time datetime="2026-12-05T10:00+05:00">Saturday 5 December at 10:00 am PKT</time> for a repotting workshop. The session runs <time datetime="PT2H">two hours</time>.</p>
</article>
</body>
</html>
5.4 Common <time> mistakes
Two mistakes cover nearly every bad <time> element on the web. The first is leaving out datetime while writing a human date as the content — the element then carries no machine-readable value at all, and a validator will flag it. The second is using the obsolete pubdate attribute, which was removed from the specification years ago; the published date is now just a <time> element like any other, and its meaning comes from where it sits in the article, not from an attribute. A related trap is wrapping a vague phrase — “yesterday”, “last week”, “soon” — in a <time> element with no datetime: relative words are not dates, and the element cannot make them into one.
6. <data>: Machine-Readable Values for Humans to Read
The <data> element links a piece of content to a machine-readable translation of it. The human-readable text goes between the tags; the machine-readable value goes in the value attribute. A product name with its SKU, a team name with its database id, a price with its numeric value — the reader sees the words, and a script or a search engine reads the exact value.
<p><data value="SKU-4812">Wireless Mouse</data> — in stock</p>
<p>Price: <data value="29.99">$29.99</data></p>
The value attribute is required. Without it, <data> is an empty wrapper that adds nothing, and a validator will flag it. The value itself can be anything the machine needs — a product code, a number, an identifier — as long as it is the machine-readable equivalent of the visible text. A value that contradicts the text (a SKU for a different product, a price that does not match) is worse than no <data> element at all, because it feeds wrong information to every tool that trusts the attribute.
6.1 <data> vs data-* attributes: do not confuse them
This is the mix-up that catches even experienced developers, because the names are nearly identical and the site already has a full guide to custom data attributes in HTML5. They are different tools for different jobs:
<data value="...">is an element that says: this visible text has this machine-readable value. It is for content — product codes, prices, identifiers — and it is visible in the page’s meaning.data-*attributes (for exampledata-product-id="4812") are attributes on any element that store private data for scripts. They are for behaviour — a script reads them to decide what to do — and they carry no meaning for search engines or assistive technology.
Use <data> when the value is part of the content and a machine should be able to read it as content. Use data-* attributes when a script needs a private hook on an element. A product listing can legitimately use both: <data> for the SKU a search engine should see, and a data-* attribute for the id a “add to cart” script needs. For the full treatment of the attribute side, see the guide to understanding custom data attributes in HTML5.
6.2 A complete example: a product list with SKUs
This complete document is a small product list where every product name is a <data> element carrying its SKU, and every price is a <data> element carrying its numeric value. A script at the bottom reads the SKUs straight out of the markup — no ids hidden in JavaScript, no scraping of visible text — which is exactly the point of the element: the data lives in the HTML, where any tool can find it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Product List with the data Element</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 560px; margin: 2rem auto; padding: 0 1rem; }
li { margin-bottom: 1rem; }
.price { font-weight: 600; }
button { padding: 0.4rem 0.9rem; margin-left: 0.5rem; }
</style>
</head>
<body>
<h1>Featured products</h1>
<ul id="products">
<li>
<data value="SKU-4812">Wireless Mouse</data> —
<data class="price" value="29.99">$29.99</data>
<button type="button" data-sku="SKU-4812">Add to cart</button>
</li>
<li>
<data value="SKU-4813">Mechanical Keyboard</data> —
<data class="price" value="89.00">$89.00</data>
<button type="button" data-sku="SKU-4813">Add to cart</button>
</li>
<li>
<data value="SKU-4814">USB-C Hub</data> —
<data class="price" value="45.50">$45.50</data>
<button type="button" data-sku="SKU-4814">Add to cart</button>
</li>
</ul>
<p id="cart">Cart is empty.</p>
<script>
const cart = document.getElementById('cart');
const items = [];
document.querySelectorAll('#products button').forEach(button => {
button.addEventListener('click', () => {
const row = button.closest('li');
const name = row.querySelector('data').textContent;
const sku = row.querySelector('data').value;
const price = row.querySelector('.price').value;
items.push(name + ' (' + sku + ') — $' + price);
cart.textContent = 'Cart: ' + items.join('; ');
});
});
</script>
</body>
</html>
Notice the deliberate split in that document: the <data> elements carry the content values (name, SKU, price) that any tool can read, while the buttons carry data-sku attributes — private script hooks that tell the click handler which product was chosen. Same page, same SKU, two different mechanisms, each doing the job it was designed for.
7. Styling the HTML Progress Bar and Meter
Out of the box, <progress> and <meter> look different in every browser: Chrome’s bar is not Firefox’s bar, and Safari’s is different again. The default look is acceptable, but most sites want the bars to match their design, and that is where the work starts — because the styling hooks for these elements are non-standard pseudo-elements, and they differ between browser engines.
7.1 The pseudo-elements
For <progress>, WebKit and Blink browsers (Chrome, Edge, Safari) expose ::-webkit-progress-bar (the track) and ::-webkit-progress-value (the filled part), plus ::-webkit-progress-inner-element. Firefox exposes ::-moz-progress-bar. For <meter>, WebKit exposes ::-webkit-meter-bar and separate value pseudo-elements for the zones — ::-webkit-meter-optimum-value, ::-webkit-meter-suboptimum-value, and ::-webkit-meter-even-less-good-value — while Firefox exposes ::-moz-meter-bar.
/* A crimson progress bar that looks the same in Chrome and Firefox */
progress {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 1.25rem;
border: 1px solid #ccc;
border-radius: 999px;
background: #f3f3f3;
}
progress::-webkit-progress-bar {
background: #f3f3f3;
border-radius: 999px;
}
progress::-webkit-progress-value {
background: crimson;
border-radius: 999px;
}
progress::-moz-progress-bar {
background: crimson;
border-radius: 999px;
}
The -webkit-appearance: none and appearance: none lines matter: without them, the browser’s native rendering wins and most of your styles are ignored. The same reset applies to <meter> — strip the native appearance first, then style the track and the value. Note that Firefox does not support ::before and ::after on meter gauges, and CSS transitions on these pseudo-elements are unreliable, so keep the styling simple: colours, height, border radius, and borders are safe; animations are not.
7.2 When to build the bar from a div instead
There is a point where the native elements stop being enough: a design that needs an exact colour in every browser, an animated fill, a striped pattern, a label inside the bar, or a vertical bar. At that point, build the bar from a div — but give the div the semantics you are giving up, with ARIA roles and values:
<div role="progressbar" aria-valuenow="70" aria-valuemin="0" aria-valuemax="100" aria-label="Upload progress">
<div style="width: 70%; height: 1rem; background: crimson;"></div>
</div>
For a gauge, the role is meter with the same three aria-value attributes. The rule is simple: use the native element whenever it can do the job, because it brings the semantics, the keyboard behaviour, and the screen-reader announcements for free; reach for the div with ARIA only when the design genuinely cannot be done natively, and then carry the semantics across by hand. <time> and <data> need no styling discussion at all — they render as inline text, and they take whatever font and colour the surrounding text has.
8. Accessibility: Labels, Screen Readers, and Fallback Text
These four elements are accessibility-positive by design — they exist precisely so that assistive technology can understand content that a div cannot express — but only if you use them completely. Three rules cover nearly everything.
First, name every bar. <progress> and <meter> both have implicit ARIA roles (progressbar and meter), so a screen reader announces the element type and its current value with no ARIA from you — but it cannot invent a name. Attach a <label>, or aria-label / aria-labelledby, so the announcement is “upload progress, progress bar, 70 percent” instead of an anonymous “progress bar, 70 percent”. For a full treatment of labelling and landmarks, see the HTML accessibility checklist.
Second, include the text. The fallback text between the tags (“70%”, “80% full”) is what users of browsers without support see, and it is the value a user can copy, search, or read when the bar itself is not visible. Keep it in sync with the value — a bar that says 70% while its text says 40% is worse than no text at all. The same applies to <meter> zones: never let colour alone carry the meaning. A red meter that only says “25%” does not tell a colour-blind user whether 25% is good or bad; the text next to it must say “25% — low battery”.
Third, remember that <time> and <data> help assistive technology in a different way: they make the content unambiguous. A screen reader reading “14 November 2026” from a <time datetime="2026-11-14"> element can offer to add it to a calendar; a price inside <data value="29.99"> can be read as a number rather than as a string of characters. Neither element changes what is spoken by default, but both make the content machine-readable, which is what downstream tools build on.
9. Seven Common Mistakes With progress, meter, time, and data
9.1 Mistake 1: Using <progress> for a gauge
Disk usage, battery level, a test score, a star rating — none of these is a task moving toward completion, and marking them up as <progress> tells every screen reader and search engine the wrong story. Use <meter>.
9.2 Mistake 2: Using <meter> for progress
A file upload, a multi-step form, an export — these have a known total and a value moving toward it. Marking them up as <meter> tells assistive technology the value is a reading, not a task, and the user gets no sense that anything will finish. Use <progress>.
9.3 Mistake 3: Leaving a bar without a label
A <progress> or <meter> with no <label> and no aria-label is announced without a name. The user hears a percentage with no idea what it measures. Label every bar, every time.
9.4 Mistake 4: Forgetting the fallback text
The text between the tags is the only thing users of old browsers see, and it is the only form of the value that can be copied or searched. An empty <progress></progress> or <meter></meter> fails silently in exactly the situation where the bar matters most.
9.5 Mistake 5: A <time> element without datetime
<time>14 November 2026</time> with no datetime attribute carries no machine-readable value — the element is an empty promise, and a validator will flag it. Always write datetime, and keep the visible text human-friendly.
9.6 Mistake 6: Confusing <data> with data-* attributes
<data value="SKU-4812"> is content a machine should read; data-sku="SKU-4812" is a private hook for a script. They share a name and nothing else. Picking the wrong one either hides content from search engines or pollutes the page’s meaning with script data.
9.7 Mistake 7: Styling for one browser only
Writing only the ::-webkit- pseudo-elements leaves Firefox with the native bar; writing only ::-moz- leaves Chrome with it. If you style the bars, write both sets, test in both engines, and accept that the result will still differ slightly — or use a div with ARIA roles when pixel-perfect consistency is a requirement.
10. Frequently Asked Questions
10.1 What is the difference between progress and meter in HTML?
<progress> shows how far through a task something is — a file upload moving from 0% to 100%. <meter> shows a measurement within a known range — disk usage at 80%, a battery at 25%. Progress moves toward completion; a meter is a reading that can go up or down. They render similarly but have different meanings, different attributes (meter has min, low, high, and optimum; progress does not), and screen readers announce them differently.
10.2 How do I make an HTML progress bar with a percentage?
Use <progress value="70" max="100">70%</progress>, with a <label> naming it. Update it from JavaScript by setting the value property — bar.value = 70 — and update the text between the tags at the same time so the fallback stays accurate. If the total is unknown, leave value off for an indeterminate bar.
10.3 Can I style the HTML progress bar with CSS?
Yes, with limits. Set appearance: none first, then style ::-webkit-progress-bar and ::-webkit-progress-value for Chrome, Edge, and Safari, and ::-moz-progress-bar for Firefox. These pseudo-elements are non-standard and the result still varies slightly between browsers. For full control, build the bar from a div with role="progressbar" and the aria-valuenow, aria-valuemin, and aria-valuemax attributes.
10.4 What is the HTML time element used for?
The <time> element marks up dates, times, and durations so machines can read them exactly. The visible text can be any format (“14 November 2026”), while the datetime attribute holds the standard value (2026-11-14). Search engines, calendars, and browser extensions use it to understand article dates, event times, and reading times.
10.5 What is the HTML data element used for?
The <data> element pairs human-readable text with a machine-readable value in its value attribute — for example <data value="SKU-4812">Wireless Mouse</data>. It is for content values like product codes, prices, and identifiers. It is not the same as data-* attributes, which are private hooks for scripts.
Conclusion
Four small elements, four distinct jobs — and the habit that ties them together is choosing meaning over appearance. An html progress bar built from <progress> tells every browser, screen reader, and search engine that a task is partway done; a <meter> says a reading sits somewhere on a scale; a <time> element says this text is a date a machine can trust; a <data> element says this label has an exact value behind it. None of them needs JavaScript, none of them needs a framework, and all four replace divs and spans that carry none of that meaning.
The next time you reach for a div with a width percentage, stop and ask the one question: is this a task finishing, a reading on a scale, a date, or a value? The answer picks the element. Get that choice right, label the bar, include the fallback text, and the small semantic elements will do the rest — including the parts of accessibility and machine-readability that no amount of CSS can add afterwards.
Related Articles You’ll Love
- HTML Accessibility Checklist: Landmarks, ARIA, and Keyboard Navigation — the labelling and landmark rules that make the bars in this guide work for screen-reader users.
- Mastering HTML Details and Summary Tags: The Complete Guide to Toggleable Content — another small semantic element pair, for content users can open and close.
- Understanding Custom Data Attributes in HTML5 (data-*): A Complete Guide — the attribute side of the
<data>story: private data hooks for your scripts. - Mastering Forms in HTML: A Comprehensive Guide to All Form Fields — where
<progress>and<meter>live most often, as labelable, form-associated elements. - HTML Headings H1 to H6: Hierarchy, SEO Rules, and Common Mistakes — the semantic foundation every page in this series is built on.