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.
<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).