Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Progress bar

Progress bar is a gauge for a scalar measurement, built on the browser's own native gauge element, so the fill's width and its color both come from the platform, not from any custom percentage math. Optional content can sit in any of the four corners around the track, and every visual aspect is themeable.

Overview API Examples Accessibility
Storage used 62%

When to use

Reach for progress bar to represent a scalar measurement within a known range, such as storage used, upload progress, a completion percentage, or a score against a target. It always needs a determinate value, since the native gauge it's built on has no "indeterminate, still figuring it out" state, unlike a spinner. For an unbounded "work is happening" indicator, use loader instead. See Alternatives below for a side by side comparison.

Install & usage

Pick a framework in the toolbar above and these snippets adapt.

npm install @eunomia/elements
import "@eunomia/elements/progress-bar.js";
<eun-progress-bar value="62" aria-label="Storage used"></eun-progress-bar>

Importing the file registers <eun-progress-bar> as a custom element, with no further setup needed. It works with any framework, or none, since it's a standard web component.

Alternatives

You want .. Prefers An unbounded "work is happening, duration unknown" indicator eun-loader

Guidance

  • Always set aria-label, unless a top-left/top-right slot already renders a visible label the gauge can borrow (see the Accessibility tab)
  • Reach for low/high/optimum when the color should follow a data-driven threshold (e.g. disk usage turning critical past 90%), and severity when you already know the state and just want to force a color
  • Keep corner content short: a label, a percentage, a min/max value. The rows size to their content and don't wrap gracefully at narrow widths
  • Using eun-progress-bar for an unbounded "loading, duration unknown" state: see Alternatives above, since <meter> has no indeterminate state
  • Setting both severity and low/high/optimum expecting both to apply: severity always wins, forcing a single color regardless of where value falls
  • Leaving aria-label unset with no visible corner label either, since screen reader users get an unlabeled gauge with no way to tell what it measures

Live testing

Properties

Progress bar <eun-progress-bar>

Attributes

NameTypeDefaultDescription
aria-labelstringThe accessible name for the gauge. Required unless a top or bottom slot already provides a visible label
valuenumber0The current value
minnumber0The lowest value the gauge can reach
maxnumber100The highest value the gauge can reach
lownumberThe upper bound of the too low range. Below it, the bar renders in its critical or warning color depending on where optimum sits
highnumberThe lower bound of the too high range. Above it, the bar renders in its critical or warning color depending on where optimum sits
optimumnumberWhich side, low or high, is considered the desired one
severity'info' | 'success' | 'warning' | 'critical' | 'neutral'Forces a single fill color, regardless of low, high, or optimum. Left unset, the color follows the native meter classification, or stays the default fill color if none of those are set

Import the exact TypeScript type behind any property above, see Types.

Slots

NameDescription
top-leftOptional content above the bar, left-aligned, such as a label
top-rightOptional content above the bar, right-aligned, such as a percentage
bottom-leftOptional content below the bar, left-aligned, such as the minimum
bottom-rightOptional content below the bar, right-aligned, such as the maximum

CSS custom properties

NameDescription
--progress-bar-track-heightSets the height of the track
--progress-bar-track-colorSets the color of the track
--progress-bar-border-radiusSets the corner radius of the track and fill
--progress-bar-fill-colorSets the color of the default or optimum fill
--progress-bar-info-colorSets the color of the info state
--progress-bar-success-colorSets the color of the success state
--progress-bar-warning-colorSets the color of the warning state
--progress-bar-critical-colorSets the color of the critical state
--progress-bar-neutral-colorSets the color of the neutral state
--progress-bar-transition-durationSets the duration of the fill's width transition
--progress-bar-corner-gapSets the gap between the corners and the track
--progress-bar-corner-colorSets the color of the corner slots
--progress-bar-corner-font-sizeSets the font size of the corner slots

Basic

value (default 0) against min/max (default 0/100). The fill's width is computed natively by the browser.

<eun-progress-bar value="40" aria-label="Progress"></eun-progress-bar>

Corner labels

Any of the four top-left/top-right/bottom-left/bottom-right slots can hold arbitrary content, such as plain text, an icon, or a eun-badge. A row stays collapsed (no reserved space) until at least one of its two slots holds content, so using only top-right doesn't leave a gap where top-left would have been.

document.pdf 75% 6.2 MB 8.3 MB
<eun-progress-bar value="75" aria-label="Upload progress">
  <span slot="top-left">document.pdf</span>
  <span slot="top-right">75%</span>
  <span slot="bottom-left">6.2 MB</span>
  <span slot="bottom-right">8.3 MB</span>
</eun-progress-bar>

With an icon

Corner slots accept any markup, not just text.

Backup Complete
<eun-progress-bar value="100" severity="success" aria-label="Backup complete">
  <span slot="top-left">Backup</span>
  <span slot="top-right" class="complete">
    <eun-icon name="check_circle" size="16"></eun-icon>
    Complete
  </span>
</eun-progress-bar>

Low, high, and optimum

Without these set, every value renders in the default fill color. Setting them lets the native <meter> classify value as optimum, sub-optimum (warning), or sub-sub-optimum (critical), with no JS thresholds to maintain. optimum past high means "higher is better", while optimum below low means "lower is better".

Battery 85% Error rate 85%
<!-- Higher is better: 85 sits above `high`, so it renders as optimum -->
<eun-progress-bar
  value="85"
  low="30"
  high="70"
  optimum="100"
  aria-label="Battery level"
></eun-progress-bar>

<!-- Lower is better: 85 sits above `high`, so it renders as critical -->
<eun-progress-bar
  value="85"
  low="30"
  high="70"
  optimum="0"
  aria-label="Error rate"
></eun-progress-bar>

Severity

Forces a single color, ignoring low/high/optimum entirely. Use it when the state is already known rather than derived from thresholds.

<eun-progress-bar
  value="40"
  severity="info"
  aria-label="Info"
></eun-progress-bar>
<eun-progress-bar
  value="100"
  severity="success"
  aria-label="Success"
></eun-progress-bar>
<eun-progress-bar
  value="60"
  severity="warning"
  aria-label="Warning"
></eun-progress-bar>
<eun-progress-bar
  value="20"
  severity="critical"
  aria-label="Critical"
></eun-progress-bar>
<eun-progress-bar
  value="50"
  severity="neutral"
  aria-label="Neutral"
></eun-progress-bar>

Part of the shared vocabulary covered in Variants, alongside every other component that reuses it.

Custom

Override the --progress-bar-* CSS variables, listed in full in the API tab.

Goal 55 / 100
<eun-progress-bar
  value="55"
  aria-label="Custom styled progress"
  class="custom-progress"
>
  <span slot="top-left">Goal</span>
  <span slot="top-right">55 / 100</span>
</eun-progress-bar>
.custom-progress {
  --progress-bar-track-height: 14px;
  --progress-bar-track-color: #ede9fe;
  --progress-bar-fill-color: #7c3aed;
  --progress-bar-border-radius: 4px;
  --progress-bar-corner-color: #7c3aed;
  --progress-bar-corner-font-size: 13px;
}

Native <meter>, not a custom progressbar

eun-progress-bar renders a real <meter> in its shadow DOM rather than building the progressbar ARIA role by hand on a <div>. That means value/min/max are exposed to assistive technology natively: there's no aria-valuenow/aria-valuemin/aria-valuemax to keep in sync, since the platform derives them straight from the element's own attributes and can't drift out of sync with what's rendered.

Note that <meter>'s implicit role is meter, not progressbar. It's the right semantic for a scalar measurement within a known range (disk usage, a score, a battery level), which is what this component is for. If what you're building is closer to "a task is N% done and actively progressing" in the progressbar sense, the distinction rarely matters in practice to users, but keep in mind the two roles aren't interchangeable everywhere: progressbar additionally supports an indeterminate state that meter does not.

Always provide an accessible name

Set aria-label on every instance, unless a visible label already sits in the top-left or top-right slot. In that case, reference it instead with aria-labelledby pointing at the slotted element's own id, so the same text isn't announced twice. An unlabeled gauge is announced by screen readers as just a number with no indication of what it measures.

Storage used 62%
<eun-progress-bar value="62" aria-labelledby="storage-label">
  <span slot="top-left" id="storage-label">Storage used</span>
  <span slot="top-right">62%</span>
</eun-progress-bar>

Dynamic updates aren't announced on their own

Neither meter nor progressbar implies a live region: screen readers don't automatically re-announce the value as it changes, and support for polling a meter's value even on focus is inconsistent across screen readers. If the progress needs to be heard as it updates, such as during a multi-step upload, pair the gauge with your own visually-hidden aria-live="polite" region (e.g. in a corner slot) that you update alongside value, rather than relying on the gauge itself.

<eun-progress-bar value="40" aria-label="Upload progress">
  <span slot="top-right" aria-live="polite">40%</span>
</eun-progress-bar>

Reference links

MDN: the <meter> element
WAI-ARIA: meter role
WAI-ARIA Authoring Practices: Read Me First (live regions)