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.
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
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.
npm install @eunomia/elements
<script type="module">
import "@eunomia/elements/progress-bar.js";
</script>
<eun-progress-bar value="62" aria-label="Storage used"></eun-progress-bar>
npm install @eunomia/elements
import "@eunomia/elements/progress-bar.js";
function StorageGauge() {
return <eun-progress-bar value={62} aria-label="Storage used" />;
}
npm install @eunomia/elements
"use client";
import "@eunomia/elements/progress-bar.js";
export function StorageGauge() {
return <eun-progress-bar value={62} aria-label="Storage used" />;
}
npm install @eunomia/elements
<script setup>
import "@eunomia/elements/progress-bar.js";
</script>
<template>
<eun-progress-bar value="62" aria-label="Storage used"></eun-progress-bar>
</template>
npm install @eunomia/elements
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@eunomia/elements/progress-bar.js";
@Component({
selector: "app-storage-gauge",
template: `<eun-progress-bar
value="62"
aria-label="Storage used"
></eun-progress-bar>`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class StorageGaugeComponent {}
Alternatives
eun-loaderGuidance
- Always set
aria-label, unless atop-left/top-rightslot already renders a visible label the gauge can borrow (see the Accessibility tab) - Reach for
low/high/optimumwhen the color should follow a data-driven threshold (e.g. disk usage turning critical past 90%), andseveritywhen 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-barfor an unbounded "loading, duration unknown" state: see Alternatives above, since<meter>has no indeterminate state - Setting both
severityandlow/high/optimumexpecting both to apply:severityalways wins, forcing a single color regardless of wherevaluefalls - Leaving
aria-labelunset 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
| Name | Type | Default | Description |
|---|---|---|---|
| aria-label | string | — | The accessible name for the gauge. Required unless a top or bottom slot already provides a visible label |
| value | number | 0 | The current value |
| min | number | 0 | The lowest value the gauge can reach |
| max | number | 100 | The highest value the gauge can reach |
| low | number | — | The upper bound of the too low range. Below it, the bar renders in its critical or warning color depending on where optimum sits |
| high | number | — | The lower bound of the too high range. Above it, the bar renders in its critical or warning color depending on where optimum sits |
| optimum | number | — | Which 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
Slots
| Name | Description |
|---|---|
| top-left | Optional content above the bar, left-aligned, such as a label |
| top-right | Optional content above the bar, right-aligned, such as a percentage |
| bottom-left | Optional content below the bar, left-aligned, such as the minimum |
| bottom-right | Optional content below the bar, right-aligned, such as the maximum |
CSS custom properties
| Name | Description |
|---|---|
| --progress-bar-track-height | Sets the height of the track |
| --progress-bar-track-color | Sets the color of the track |
| --progress-bar-border-radius | Sets the corner radius of the track and fill |
| --progress-bar-fill-color | Sets the color of the default or optimum fill |
| --progress-bar-info-color | Sets the color of the info state |
| --progress-bar-success-color | Sets the color of the success state |
| --progress-bar-warning-color | Sets the color of the warning state |
| --progress-bar-critical-color | Sets the color of the critical state |
| --progress-bar-neutral-color | Sets the color of the neutral state |
| --progress-bar-transition-duration | Sets the duration of the fill's width transition |
| --progress-bar-corner-gap | Sets the gap between the corners and the track |
| --progress-bar-corner-color | Sets the color of the corner slots |
| --progress-bar-corner-font-size | Sets 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.
<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.
<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".
<!-- 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
Custom
Override the --progress-bar-* CSS variables, listed in full in the API tab.
<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.
<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
<meter> element