<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 (§6c item 5), 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. |