Progress
Part of the extras bundle, not included by default.
Intent
A progress display can take a horizontal-bar shape -- file uploads, downloads, multi-step forms, deployment pipelines -- or a circular-ring shape for storage gauges, quota indicators, dashboard cards. Both communicate "how much of something is done" via a fraction-of-complete value.
Bar and ring share token names and conventions:
--progress-fill for the active color,
--progress-bg for the track. Status variants
(.is-success, .is-warning,
.is-danger) remap the chromatic axis on either
shape with the same class names. The bar additionally
supports indeterminate (sliding-loop) and stacked
(multi-segment) modes; the ring is determinate-only --
circular indeterminate loading is what the
spinner component is for.
Bar -- basic usage
The bar is two divs: an outer .progress with
ARIA wiring and an inner .progress-bar whose
inline-size is set to the percentage complete.
<div class="progress" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" aria-label="Upload">
<div class="progress-bar" style="inline-size: 60%"></div>
</div>With inline label
The .progress-bar is a flex container that
centers any text inside. The default fill is soft (less
saturated, designed for compositional elegance over state-
alarm volume), and the label inherits
--color-text. For labeled bars where you want
guaranteed contrast, override
--progress-fill back to the loud token and pair
with --color-on-primary (or the matching
on-color) for the label. Inline labels work best at the
default and large sizes; small bars (0.25rem tall) don't
have room for text.
<div class="stack">
<div class="progress is-lg" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" style="--progress-fill: var(--color-primary); --progress-label-color: var(--color-on-primary);">
<div class="progress-bar" style="inline-size: 60%">60%</div>
</div>
<div class="progress is-lg" role="progressbar" aria-valuenow="35" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar" style="inline-size: 35%">35%</div>
</div>
</div>The first bar above shows the loud-with-label pattern (label on full-saturation primary). The second uses the default soft fill -- the label is harder to read inside the bar, which is why the convention is to put labels above or below soft progress bars rather than inside them.
Sizes
<div class="stack">
<div class="progress is-sm" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar" style="inline-size: 40%"></div>
</div>
<div class="progress" role="progressbar" aria-valuenow="55" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar" style="inline-size: 55%"></div>
</div>
<div class="progress is-lg" role="progressbar" aria-valuenow="70" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar" style="inline-size: 70%"></div>
</div>
</div>Status colors
Status variants on .progress-bar remap
--progress-fill to the matching soft token
(--color-success-soft,
--color-warning-soft,
--color-danger-soft). Use these for "good /
neutral / bad" data displays where chromatic meaning carries
semantic context but the volume should match surrounding
chrome. For loud status (state-alarm volume -- failed
deployment, danger threshold breached) override
--progress-fill directly to
--color-danger on the instance.
<div class="stack">
<div class="progress is-lg" role="progressbar" aria-valuenow="80" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar is-success" style="inline-size: 80%">80%</div>
</div>
<div class="progress is-lg" role="progressbar" aria-valuenow="65" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar is-warning" style="inline-size: 65%">65%</div>
</div>
<div class="progress is-lg" role="progressbar" aria-valuenow="90" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar is-danger" style="inline-size: 90%">90%</div>
</div>
</div>Indeterminate
For loading states with no known fraction, add
.is-indeterminate on the
.progress root. The inner bar absolutely
positions itself and slides across the track on a 1.5s
loop. Add aria-valuetext="Loading" so screen
readers describe the state instead of the value.
Under prefers-reduced-motion: reduce, the
sliding animation falls back to a static-position opacity
pulse -- still a "something's happening" affordance, no
horizontal travel.
<div class="progress is-indeterminate" role="progressbar" aria-label="Loading" aria-valuetext="Loading">
<div class="progress-bar"></div>
</div>Stacked segments
For multi-segment progress -- "67% used + 12% reserved + 21%
free" patterns -- add .is-stacked on the
.progress root and put multiple
.progress-bar children inside. The first and
last segments keep the track's outer radius; middle segments
sit square so seams meet flat.
<div class="progress is-lg is-stacked" role="progressbar" aria-valuenow="79" aria-valuemin="0" aria-valuemax="100" aria-label="Storage usage">
<div class="progress-bar is-success" style="inline-size: 50%"></div>
<div class="progress-bar is-warning" style="inline-size: 22%"></div>
<div class="progress-bar is-danger" style="inline-size: 7%"></div>
</div>Ring -- basic usage
The ring is a .progress-ring whose
--progress-value token (0–100) drives a
conic-gradient that paints the arc. A
::after pseudo-element punches the inner cutout,
turning the disc into a ring. Optional inner
<span> shows a value label.
<div class="cluster">
<div class="progress-ring" style="--progress-value: 25" role="progressbar" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100">
<span>25%</span>
</div>
<div class="progress-ring" style="--progress-value: 50" role="progressbar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100">
<span>50%</span>
</div>
<div class="progress-ring" style="--progress-value: 75" role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100">
<span>75%</span>
</div>
<div class="progress-ring" style="--progress-value: 100" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100">
<span>100%</span>
</div>
</div>Ring sizes
<div class="cluster">
<div class="progress-ring is-sm" style="--progress-value: 60" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100">
<span>60%</span>
</div>
<div class="progress-ring" style="--progress-value: 60" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100">
<span>60%</span>
</div>
<div class="progress-ring is-lg" style="--progress-value: 60" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100">
<span>60%</span>
</div>
</div>Ring status colors
<div class="cluster">
<div class="progress-ring is-success" style="--progress-value: 80" role="progressbar" aria-valuenow="80" aria-valuemin="0" aria-valuemax="100">
<span>80%</span>
</div>
<div class="progress-ring is-warning" style="--progress-value: 65" role="progressbar" aria-valuenow="65" aria-valuemin="0" aria-valuemax="100">
<span>65%</span>
</div>
<div class="progress-ring is-danger" style="--progress-value: 90" role="progressbar" aria-valuenow="90" aria-valuemin="0" aria-valuemax="100">
<span>90%</span>
</div>
</div>Ring inside a card
The ring's center cutout defaults to
--color-surface (the page background). Inside a
card or other elevated container, override
--progress-ring-cutout-bg on the ring (or on a
surrounding scope) so the cutout matches its surroundings --
otherwise the cutout looks stranded against the card.
Storage used
API quota
<div class="grid" style="--grid-min: 12rem">
<div class="card stack is-centered text-center" style="--avatar-status-ring: var(--color-surface-raised); --progress-ring-cutout-bg: var(--color-surface-raised)">
<div class="progress-ring is-success" style="--progress-value: 67" role="progressbar" aria-valuenow="67" aria-valuemin="0" aria-valuemax="100">
<span>67%</span>
</div>
<p class="text-muted">Storage used</p>
</div>
<div class="card stack is-centered text-center" style="--progress-ring-cutout-bg: var(--color-surface-raised)">
<div class="progress-ring is-warning" style="--progress-value: 89" role="progressbar" aria-valuenow="89" aria-valuemin="0" aria-valuemax="100">
<span>89%</span>
</div>
<p class="text-muted">API quota</p>
</div>
</div>Dashboard composition
A common pattern: stat-card with progress bar underneath.
Composes .stat, .progress, and
.card directly -- no special class needed.
<div class="grid" style="--grid-min: 14rem">
<div class="card stack">
<div class="stat is-compact">
<span class="stat-label">Storage</span>
<span class="stat-value">67%</span>
</div>
<div class="progress is-sm" role="progressbar" aria-valuenow="67" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar" style="inline-size: 67%"></div>
</div>
<span class="text-muted">67 GB of 100 GB</span>
</div>
<div class="card stack">
<div class="stat is-compact">
<span class="stat-label">API quota</span>
<span class="stat-value">89%</span>
</div>
<div class="progress is-sm" role="progressbar" aria-valuenow="89" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar is-warning" style="inline-size: 89%"></div>
</div>
<span class="text-muted">89k of 100k requests</span>
</div>
<div class="card stack">
<div class="stat is-compact">
<span class="stat-label">Build minutes</span>
<span class="stat-value">34%</span>
</div>
<div class="progress is-sm" role="progressbar" aria-valuenow="34" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar is-success" style="inline-size: 34%"></div>
</div>
<span class="text-muted">340 of 1,000 minutes</span>
</div>
</div>Customization
Bar tokens cover height, radius, track / fill / label
colors, and transition duration. Ring tokens cover diameter,
stroke width, fill / track colors, the cutout color, and the
consumer-set --progress-value. Status variants
on either shape remap the chromatic axis through the
semantic --color-* tokens, so the dark-mode
palette tracks automatically -- no per-mode override needed.
The most common per-instance overrides:
-
--progress-fillfor branded fill colors outside the standard status palette. -
--progress-ring-cutout-bgfor rings inside cards or other elevated surfaces. -
--progress-heighton individual bars when the size variants don't fit (e.g.,--progress-height: 0.75remfor a between-default-and-large bar).
Accessibility
-
Always include
role="progressbar"on the outer element. For determinate states, setaria-valuenow,aria-valuemin, andaria-valuemax. -
Indeterminate bars omit
aria-valuenowand addaria-valuetext="Loading"(or similar) so screen readers announce the state instead of trying to read a numeric value. -
Add
aria-labeldescribing what's progressing -- "Upload", "Storage usage", "Build progress". Without it, the progress role is announced without context. -
For circular indeterminate loading, use
.spinner -- not
.progress-ring..progress-ringis determinate-only (requires a known value).
Markup
<!-- Determinate bar -->
<div class="progress" role="progressbar"
aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"
aria-label="Upload">
<div class="progress-bar" style="inline-size: 60%"></div>
</div>
<!-- Indeterminate bar -->
<div class="progress is-indeterminate" role="progressbar"
aria-label="Loading" aria-valuetext="Loading">
<div class="progress-bar"></div>
</div>
<!-- Stacked segments -->
<div class="progress is-stacked" role="progressbar" ...>
<div class="progress-bar is-success" style="inline-size: 60%"></div>
<div class="progress-bar is-warning" style="inline-size: 30%"></div>
</div>
<!-- Ring -->
<div class="progress-ring" style="--progress-value: 75"
role="progressbar" aria-valuenow="75"
aria-valuemin="0" aria-valuemax="100">
<span>75%</span>
</div>
CSS Reference: Progress
Horizontal fill bar for determinate and indeterminate states
Source: src/css/extras/progress.css
Tokens
| Token | Default | Description |
|---|---|---|
--progress-height | 0.5rem | Track height of the progress bar |
--progress-radius | var(--radius-pill) | Corner radius for the bar track and fill |
--progress-bg | var(--color-fill) | Background color of the unfilled track |
--progress-fill | var(--color-primary-soft) | Fill color for the completed portion |
--progress-label-color | var(--color-text) | Text color for inline percentage labels |
--progress-transition | var(--transition-normal) | Transition for animated width changes on the fill |
Slots
| Slot | Description |
|---|---|
.progress-bar | Filled portion of the bar |
Classes
| Class | Description |
|---|---|
.progress | component root |
.is-sm | Smaller thickness for the progress bar |
.is-lg | Larger thickness for the progress bar |
.progress-bar.is-success | Success color variant for the internal progress-bar |
.progress-bar.is-warning | Warning color variant for the internal progress-bar |
.progress-bar.is-danger | Danger color variant for the internal progress-bar |
.is-stacked | Stacked segments, bars flush edge-to-edge inside one track. The first / last segments keep the track's outer radius; middle segments are square so the seams meet flat. |
.is-indeterminate | An indeterminate bar slides across the track on a loop. Container becomes positioning context; the .progress-bar inside is absolutely positioned so the keyframe animates its own position and size without nudging the surrounding layout. |
CSS Reference: Progress Ring
Circular progress ring for determinate states
Source: src/css/extras/progress.css
Tokens
| Token | Default | Description |
|---|---|---|
--progress-ring-size | 5rem | Outer diameter of the ring |
--progress-ring-width | 0.375rem | Thickness of the ring stroke |
--progress-fill | var(--color-primary-soft) | Fill color for the ring arc |
--progress-bg | var(--color-fill) | Background color for the unfilled portion of the ring |
--progress-ring-cutout-bg | var(--color-surface) | Background color of the inner cutout |
--progress-value | 0 | Consumer-set progress value, 0 to 100 |
Classes
| Class | Description |
|---|---|
.progress-ring | component root |
.is-success | Success color variant for the ring fill |
.is-warning | Warning color variant for the ring fill |
.is-danger | Danger color variant for the ring fill |
.is-sm | Small ring variant (3rem diameter) |
.is-lg | Large ring variant (8rem diameter) |