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.

60%
35%
<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.

80%
65%
90%
<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.

25%
50%
75%
100%
<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

60%
60%
60%
<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

80%
65%
90%
<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.

67%

Storage used

89%

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.

Storage 67%
67 GB of 100 GB
API quota 89%
89k of 100k requests
Build minutes 34%
340 of 1,000 minutes
<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-fill for branded fill colors outside the standard status palette.
  • --progress-ring-cutout-bg for rings inside cards or other elevated surfaces.
  • --progress-height on individual bars when the size variants don't fit (e.g., --progress-height: 0.75rem for a between-default-and-large bar).

Accessibility

  • Always include role="progressbar" on the outer element. For determinate states, set aria-valuenow, aria-valuemin, and aria-valuemax.
  • Indeterminate bars omit aria-valuenow and add aria-valuetext="Loading" (or similar) so screen readers announce the state instead of trying to read a numeric value.
  • Add aria-label describing 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-ring is 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

TokenDefaultDescription
--progress-height0.5remTrack height of the progress bar
--progress-radiusvar(--radius-pill)Corner radius for the bar track and fill
--progress-bgvar(--color-fill)Background color of the unfilled track
--progress-fillvar(--color-primary-soft)Fill color for the completed portion
--progress-label-colorvar(--color-text)Text color for inline percentage labels
--progress-transitionvar(--transition-normal)Transition for animated width changes on the fill

Slots

SlotDescription
.progress-barFilled portion of the bar

Classes

ClassDescription
.progresscomponent root
.is-smSmaller thickness for the progress bar
.is-lgLarger thickness for the progress bar
.progress-bar.is-successSuccess color variant for the internal progress-bar
.progress-bar.is-warningWarning color variant for the internal progress-bar
.progress-bar.is-dangerDanger color variant for the internal progress-bar
.is-stackedStacked 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-indeterminateAn 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

TokenDefaultDescription
--progress-ring-size5remOuter diameter of the ring
--progress-ring-width0.375remThickness of the ring stroke
--progress-fillvar(--color-primary-soft)Fill color for the ring arc
--progress-bgvar(--color-fill)Background color for the unfilled portion of the ring
--progress-ring-cutout-bgvar(--color-surface)Background color of the inner cutout
--progress-value0Consumer-set progress value, 0 to 100

Classes

ClassDescription
.progress-ringcomponent root
.is-successSuccess color variant for the ring fill
.is-warningWarning color variant for the ring fill
.is-dangerDanger color variant for the ring fill
.is-smSmall ring variant (3rem diameter)
.is-lgLarge ring variant (8rem diameter)