v1.0.0

<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

The fainter fill behind the first is a 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.
The last two have no 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".
Run it With a screen reader on, listen rather than watch. 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> &mdash; 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 &mdash; 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
    &mdash; changing <code>aria-valuenow</code> is silent in every major screen reader &mdash; 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

PropertyAttributeTypeDefaultDescription
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

PartDescription
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

PropertyDescription
--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.