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 (§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

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.