Progress

A horizontal bar that fills as work completes.

Example

Pair the bar with a label and a percentage above. The label states the action; the percentage sits muted on the trailing edge so the eye reads the action first.

Uploading 72%
<div class="w-100">
  <div class="d-flex justify-content-between mb-1">
    <span class="fw-medium">Uploading</span>
    <span class="text-muted-foreground">72%</span>
  </div>
  <div class="progress" role="progressbar" aria-label="Upload" aria-valuenow="72" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar" style="width: 72%"></div>
  </div>
</div>

Basic

Wrap the bar in .progress and place a single .progress__bar inside. The wrapper carries role="progressbar" and the aria-valuenow / aria-valuemin / aria-valuemax attributes; the inner element paints the fill via width.

<div class="d-flex flex-column gap-4 w-100">
  <div class="progress" role="progressbar" aria-label="Zero" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar" style="width: 0%"></div>
  </div>
  <div class="progress" role="progressbar" aria-label="Quarter" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar" style="width: 25%"></div>
  </div>
  <div class="progress" role="progressbar" aria-label="Half" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar" style="width: 50%"></div>
  </div>
  <div class="progress" role="progressbar" aria-label="Three quarters" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar" style="width: 75%"></div>
  </div>
  <div class="progress" role="progressbar" aria-label="Full" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar" style="width: 100%"></div>
  </div>
</div>

Intents

Recolor the fill with an intent modifier on .progress__bar. The track stays on the neutral tier so contrast holds in both themes.

<div class="d-flex flex-column gap-4 w-100">
  <div class="progress" role="progressbar" aria-label="Primary" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar progress__bar--primary" style="width: 60%"></div>
  </div>
  <div class="progress" role="progressbar" aria-label="Success" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar progress__bar--success" style="width: 60%"></div>
  </div>
  <div class="progress" role="progressbar" aria-label="Warning" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar progress__bar--warning" style="width: 60%"></div>
  </div>
  <div class="progress" role="progressbar" aria-label="Danger" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar progress__bar--danger" style="width: 60%"></div>
  </div>
  <div class="progress" role="progressbar" aria-label="Info" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar progress__bar--info" style="width: 60%"></div>
  </div>
</div>

Sizes

Three sizes cover the typical range. Default sits at a thin strip; --sm compresses to a hairline; --lg grows to label-friendly height.

<div class="d-flex flex-column gap-4 w-100">
  <div class="progress progress--sm" role="progressbar" aria-label="Small" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar" style="width: 60%"></div>
  </div>
  <div class="progress" role="progressbar" aria-label="Default" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar" style="width: 60%"></div>
  </div>
  <div class="progress progress--lg" role="progressbar" aria-label="Large" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar" style="width: 60%"></div>
  </div>
</div>

Animated

Add .progress__bar--animated for a soft sheen that sweeps across the fill every few seconds. It's a quiet "still working" signal. It isn't a constant treadmill. prefers-reduced-motion turns the animation off.

<div class="d-flex flex-column gap-4 w-100">
  <div class="progress progress--lg" role="progressbar" aria-label="Animated primary" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar progress__bar--animated" style="width: 60%"></div>
  </div>
  <div class="progress progress--lg" role="progressbar" aria-label="Animated success" aria-valuenow="80" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar progress__bar--success progress__bar--animated" style="width: 80%"></div>
  </div>
</div>

Indeterminate

Add .is-indeterminate on .progress when the duration is unknown. A partial bar slides across the track on loop; drop the inline width on the bar, the CSS owns it. Skip aria-valuenow so assistive tech announces the bar as indeterminate.

<div class="d-flex flex-column gap-4 w-100">
  <div class="progress is-indeterminate" role="progressbar" aria-label="Loading" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar"></div>
  </div>
  <div class="progress progress--lg is-indeterminate" role="progressbar" aria-label="Loading large" aria-valuemin="0" aria-valuemax="100">
    <div class="progress__bar progress__bar--success"></div>
  </div>
</div>

Customization

Nine variables retune .progress without touching component CSS. Override on .progress itself, on a parent scope, or on :root. The cascade scopes the change. Size modifiers --sm / --lg retune the track height in their own scope.

Variable Default Use
--progress-height calc(0.5rem * var(--st-density)) Track height (8 at default density; sm 4, lg 16)
--progress-radius 9999px Track corner radius (pill; opts out of --st-radius)
--progress-margin-bottom 0 Trailing space below the bar
--progress-bg var(--st-neutral) Track background (unfilled portion)
--progress-bar-bg var(--st-primary) Bar fill (intent modifiers retune)
--progress-bar-transition-duration 0.6s Width tween when value changes from JS
--progress-shimmer-color color-mix(in oklch, white 30%, transparent) Animated sheen color (reads on every intent in both themes)
--progress-shimmer-duration 3s Animated cycle length (sweep plus rest)
--progress-indeterminate-duration 1.5s Indeterminate sliding-bar cycle length

Track opts out of --st-radius. Pill is semantic for a progress strip. It isn't a style choice. Override --progress-radius per-component if you really want a square track (e.g. on a brutalist preset).