<bmx-progress>
How far through something is: a bar or a ring, with a value or without one.
12 properties · 0 events · 0 methods · 7 parts
Example
buffer — how much of a video has loaded against how much
has played, or how much of an upload has been accepted against how much has been sent. It is drawn and never
announced: a screen-reader user read two numbers for one operation learns less, not more.
value, which is a state rather than a missing input: work is happening
and nobody is pretending to know how much is left. aria-valuenow is omitted entirely, which is how ARIA
spells indeterminate — not aria-valuenow="0", which claims a position and is read as "nothing has
happened yet".
role="progressbar" announces nothing by itself
— changing aria-valuenow is silent in every major screen reader — so this component keeps a
live region beside it and speaks at milestones: every twenty per cent, and the finish. A hundred integers read aloud
over the top of whatever you were doing is the other half of the same defect, and the reason it is not every value.
Show markup
<div class="row" style="flex-direction: column; align-items: stretch; gap: 1rem">
<bmx-progress label="Uploading accounts-q3.xlsx" value="40" show-value="true"></bmx-progress>
<bmx-progress label="Restoring backup" tone="info" value="72" show-value="true" size="lg"></bmx-progress>
<bmx-progress label="Disk used" tone="warning" value="91" show-value="true" size="sm"></bmx-progress>
</div>
<div class="row" style="margin-block-start: 1rem">
<bmx-progress label="Buffered" value="34" buffer="68" show-value="true"></bmx-progress>
</div>
<div class="row">
<span class="note">
The fainter fill behind the first is a <code>buffer</code> — how much of a video has loaded against how much
has played, or how much of an upload has been accepted against how much has been sent. It is drawn and never
announced: a screen-reader user read two numbers for one operation learns less, not more.
</span>
</div>
<div class="row" style="margin-block-start: 1rem; align-items: center; gap: 1.5rem">
<bmx-progress label="Syncing" shape="ring" value="65" show-value="true" size="lg"></bmx-progress>
<bmx-progress label="Loading" shape="ring" size="md"></bmx-progress>
<bmx-progress label="Working" shape="ring" size="sm" tone="neutral"></bmx-progress>
<span class="note" style="flex: 1 1 14rem">
The last two have no <code>value</code>, which is a <em>state</em> rather than a missing input: work is happening
and nobody is pretending to know how much is left. <code>aria-valuenow</code> is omitted entirely, which is how ARIA
spells indeterminate — not <code>aria-valuenow="0"</code>, which claims a position and is read as "nothing has
happened yet".
</span>
</div>
<div class="row" style="margin-block-start: 1rem">
<bmx-progress id="ex-progress-run" label="Copying files" value="0" show-value="true"></bmx-progress>
</div>
<div class="row">
<bmx-button id="ex-progress-start" variant="outline" tone="neutral">Run it</bmx-button>
<span class="note" style="flex: 1 1 18rem">
With a screen reader on, listen rather than watch. <code>role="progressbar"</code> announces nothing by itself
— changing <code>aria-valuenow</code> is silent in every major screen reader — so this component keeps a
live region beside it and speaks at milestones: every twenty per cent, and the finish. A hundred integers read aloud
over the top of whatever you were doing is the other half of the same defect, and the reason it is not every value.
</span>
</div>
<script>
(function () {
const bar = document.getElementById('ex-progress-run');
let timer;
document.getElementById('ex-progress-start').addEventListener('bmxActivate', () => {
clearInterval(timer);
bar.value = 0;
timer = setInterval(() => {
bar.value = Math.min(100, bar.value + 4);
if (bar.value >= 100) {
clearInterval(timer);
}
}, 160);
});
})();
</script>
<bmx-progress label="Uploading" value="40"></bmx-progress>
<bmx-progress label="Loading" shape="ring"></bmx-progress>
THE PART THAT IS NOT THE DRAWING
Filling a bar to 40% is one declaration. Three things around it are what this component is actually for.
role="progressbar" announces nothing by itself. Changing
aria-valuenow is silent in every major screen reader, so a component that
only maintains the attribute is one whose progress can be polled and cannot
be heard. There is a live region here, and the moment there is one the
opposite failure arrives - a hundred integers spoken over the top of
whatever the user was doing. So progress is announced at milestones, every
announce-every per cent and at the finish. Set it to 0 for silence.
No value is a state, not a missing input. Leave value off and the
component is indeterminate: it says work is happening and refuses to say
how much is left, which is honest and is what aria-valuenow is omitted to
mean. A range that cannot hold a position - a maximum at or below its
minimum - lands in the same state rather than being papered over, so the
numbers being wrong is visible rather than silent.
Reduced motion is the design, not a footnote. An indeterminate indicator
is motion by definition, so prefers-reduced-motion cannot simply switch it
off: an indicator that does nothing at all says the page has hung. What it
does instead is stop travelling and breathe - a slow, small change in
opacity that reads as alive without anything crossing the screen.
NAMING IT
Give it a label. A progressbar with no accessible name is announced as a
percentage attached to nothing, which tells a screen-reader user that
something is 40% done and not what.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
announceEvery |
announce-every |
number |
20 |
How often to announce progress, in per cent. 0 never announces. Twenty is five utterances for a whole operation: enough to know it is moving, few enough to ignore. Lower it for something slow and important; set it to 0 when the same progress is already being reported somewhere else on the page, because two live regions describing one operation is worse than either alone. |
buffer |
buffer |
number |
— | A second, fainter value on the same track. For the two-speed cases: how much of a video has buffered against how much has played, how much of an upload has been accepted against how much has been sent. It is drawn and never announced - a screen-reader user being read two numbers for one operation learns less, not more. |
label |
label |
string |
— | The accessible name. Say what is progressing, not that something is. |
max |
max |
number |
100 |
The top of the range. |
min |
min |
number |
0 |
The bottom of the range. |
shape |
shape |
BmxProgressShape |
'bar' |
A bar or a ring. |
showValue |
show-value |
boolean |
false |
Whether to draw the value beside the bar, or inside the ring. |
size |
size |
BmxSize |
'md' |
The size step. |
thickness |
thickness |
number |
4 |
The ring's stroke, in units of its 48-unit viewBox. A property rather than a custom property, and the one place this library bends its own rule that looks belong in CSS. The arc is drawn as a dash pattern round a circle, and both the circle's radius and the length of that dash are computed from this number - so a thickness the component could not read would be a ring whose arc ended in the wrong place, or one clipped by its own viewBox on all four sides. getComputedStyle cannot hand back an unregistered custom property resolved - it returns the declared text, var() and all - so reading it is not an option either. The ring's size is CSS, where it belongs: --bmx-progress-ring-size, and the viewBox scales to it. |
tone |
tone |
BmxTone |
'primary' |
The semantic colour. |
value |
value |
number |
— | How far through it is. Leave it off for an indeterminate indicator. That is a state rather than a gap: plenty of work has no measurable end, and saying so is better than inventing a number that creeps to 90% and stops. |
valueText |
value-text |
string |
— | What to say instead of the percentage. "3 of 10 files" is a better thing to hear than "30%", and it is what aria-valuetext is for. It replaces the announcement and the drawn value both, so the two cannot disagree. |
CSS shadow parts
| Part | Description |
|---|---|
buffer |
The secondary fill, when there is a buffer. |
fill |
The filled part. |
ring |
The <svg>, in ring shape. |
ring-fill |
The arc itself. |
ring-track |
The circle behind the arc. |
track |
The groove the fill runs in, and the element carrying the role. |
value |
The percentage, when show-value is set. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-progress-buffer |
The second, fainter fill. |
--bmx-progress-duration |
One cycle of the indeterminate animation. |
--bmx-progress-fill |
The filled part. Follows the tone by default. |
--bmx-progress-radius |
The bar's end caps. Half the thickness is a pill; 0 is square. |
--bmx-progress-ring-size |
How large the ring is drawn. Its viewBox scales to whatever this says. |
--bmx-progress-track |
The groove behind it. |
--bmx-progress-track-size |
How thick the bar is. The size step sets it; this overrides that. |
--bmx-progress-value-color |
The percentage's text colour. |
--bmx-progress-value-size |
The percentage's text size. |