Progress — .mv-progress
Progress bars on the native <progress> (determinate with a shine, indeterminate with a sweep, segmented), a circular SVG ring driven by --value with an animated counter, and a <meter> gauge colored by its thresholds.
| Category | Feedback |
|---|---|
| Type | CSS only (.mv-progress) |
| Status | stable |
| Keywords | progress, progression, progressbar, ring, circular, meter, jauge, gauge, loading, upload |
When to use
- A task with a known completion percentage, such as an upload or import, must show how far along it is
- A compact circular ring shows a percentage inside a tile, card or profile completeness widget
- A gauge shows a level against good and bad thresholds, like storage quota or password strength
Avoid when
- The wait is short and its duration unknown, and a small inline indicator is enough → use Spinner instead
- Progress follows named steps the user moves through rather than a percentage → use Stepper instead
- The goal is to show how far the reader has scrolled through a page → use Scroll Progress instead
Install
node scripts/add.mjs progress --out ./src/marvelousAI agent with the Marvelous UI MCP server: install_components({ slugs: ["progress"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): tokens/tokens.css, core/base.css, components/progress/progress.css.
Usage
Canonical markup — start from it and customize with attributes, data-* and CSS variables:
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,19rem),1fr));gap:2.5rem 3rem;width:100%;align-items:start">
<!-- Linear bars -->
<div style="display:grid;gap:1.5rem">
<div class="mv-progress-field">
<label class="mv-progress-label" for="pr-demo-upload">annual-report-2026.pdf</label>
<output class="mv-progress-value" for="pr-demo-upload" id="pr-demo-upload-value">12%</output>
<progress class="mv-progress" id="pr-demo-upload" max="100" value="12" data-shine></progress>
<span class="mv-progress-hint" id="pr-demo-upload-hint">0.4 MB of 3.5 MB · estimating time left…</span>
</div>
<div class="mv-progress-field">
<label class="mv-progress-label" for="pr-demo-sync">Syncing contacts…</label>
<progress class="mv-progress" id="pr-demo-sync" data-size="sm" aria-describedby="pr-demo-sync-hint"></progress>
<span class="mv-progress-hint" id="pr-demo-sync-hint">Importing from Google Contacts</span>
</div>
<div style="display:grid;gap:.75rem">
<progress class="mv-progress" data-size="xs" data-color="success" max="100" value="100" aria-label="Backup complete"></progress>
<progress class="mv-progress" data-size="sm" data-color="warning" max="100" value="64" aria-label="Credits used"></progress>
<progress class="mv-progress" data-color="danger" max="100" value="92" aria-label="Disk space"></progress>
<progress class="mv-progress" data-size="lg" data-variant="segmented" max="100" value="40" aria-label="Profile 40% complete"></progress>
</div>
</div>
<!-- Meters -->
<div style="display:grid;gap:1.5rem">
<div class="mv-progress-field">
<label class="mv-progress-label" for="pr-demo-storage">Storage</label>
<span class="mv-progress-value">7.2 GB of 10 GB</span>
<meter class="mv-meter" id="pr-demo-storage" min="0" max="10" low="6" high="8.5" optimum="2" value="7.2"></meter>
<span class="mv-progress-hint">Above 8.5 GB, syncing slows down.</span>
</div>
<div class="mv-progress-field">
<label class="mv-progress-label" for="pr-demo-api">Monthly API quota</label>
<span class="mv-progress-value">94%</span>
<meter class="mv-meter" id="pr-demo-api" min="0" max="100" low="70" high="90" optimum="10" value="94"></meter>
</div>
<div class="mv-progress-field">
<label class="mv-progress-label" for="pr-demo-pwd">Password strength</label>
<span class="mv-progress-value" style="color:var(--mv-success)">Strong</span>
<meter class="mv-meter" id="pr-demo-pwd" data-variant="segmented" data-color="success" data-size="sm" style="--mv-progress-segments:4" min="0" max="100" low="40" high="70" optimum="100" value="85"></meter>
</div>
</div>
<!-- Rings -->
<div style="grid-column:1/-1;display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:2.5rem">
<div class="mv-progress-ring" id="pr-demo-ring" role="progressbar" aria-label="Monthly goal" aria-valuemin="0" aria-valuemax="100" aria-valuenow="72" style="--value: 72">
<svg viewBox="0 0 36 36" aria-hidden="true"><circle class="mv-progress-ring-track" cx="18" cy="18" r="16"/><circle class="mv-progress-ring-bar" cx="18" cy="18" r="16" pathLength="100"/></svg>
<span class="mv-progress-ring-label"></span>
</div>
<div class="mv-progress-ring" data-size="lg" data-color="success" role="progressbar" aria-label="Tasks completed" aria-valuemin="0" aria-valuemax="100" aria-valuenow="67" aria-valuetext="8 of 12 tasks" style="--value: 67">
<svg viewBox="0 0 36 36" aria-hidden="true"><circle class="mv-progress-ring-track" cx="18" cy="18" r="16"/><circle class="mv-progress-ring-bar" cx="18" cy="18" r="16" pathLength="100"/></svg>
<span class="mv-progress-ring-label" style="display:grid;justify-items:center;gap:4px">8/12<small style="font-size:.6875rem;font-weight:500;color:var(--mv-fg-muted);letter-spacing:0">tasks</small></span>
</div>
<div class="mv-progress-ring" data-size="sm" data-color="warning" role="progressbar" aria-label="Battery" aria-valuemin="0" aria-valuemax="100" aria-valuenow="28" style="--value: 28">
<svg viewBox="0 0 36 36" aria-hidden="true"><circle class="mv-progress-ring-track" cx="18" cy="18" r="16"/><circle class="mv-progress-ring-bar" cx="18" cy="18" r="16" pathLength="100"/></svg>
<span class="mv-progress-ring-label"></span>
</div>
<div class="mv-progress-ring" data-indeterminate role="progressbar" aria-label="Analyzing">
<svg viewBox="0 0 36 36" aria-hidden="true"><circle class="mv-progress-ring-track" cx="18" cy="18" r="16"/><circle class="mv-progress-ring-bar" cx="18" cy="18" r="16" pathLength="100"/></svg>
</div>
</div>
</div>
<script type="module">
// Simulate an upload so the bar transition is visible.
const bar = document.getElementById("pr-demo-upload");
const out = document.getElementById("pr-demo-upload-value");
const hint = document.getElementById("pr-demo-upload-hint");
if (bar) setTimeout(() => {
bar.value = 68;
out.textContent = "68%";
hint.textContent = "2.4 MB of 3.5 MB · 12 s left";
}, 350);
</script>API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
data-size | xs | sm | lg | (md) | Bar thickness or ring size (sm | lg). |
data-color | success | warning | danger | info | neutral | (accent) | Fill color. |
data-shine | boolean | Shine that runs across an in-progress determinate bar. | |
data-variant | segmented | Splits the bar / gauge into segments. | |
data-indeterminate | boolean | On .mv-progress-ring: continuously spinning arc. |
CSS classes
| Name | Description |
|---|---|
mv-progress | On <progress value max>; without value = indeterminate (sweep). |
mv-meter | On <meter min max low high optimum value>: accent when optimal, then warning and danger based on the native thresholds. |
mv-progress-ring | Ring: a role="progressbar" container + style="--value: 0…100" holding an <svg viewBox="0 0 36 36"> with two r=16 circles (the second with pathLength="100"). |
mv-progress-ring-track / -bar | Track and value circles of the ring. |
mv-progress-ring-label | Center label; empty = automatically animated percentage (CSS counter). |
mv-progress-field | Label + value grid above the bar, hint text below. |
mv-progress-label / -value / -hint | Field parts. |
CSS variables
| Name | Default | Description |
|---|---|---|
--value | 0 | Ring value (0 to 100), with a smooth transition and animated counter on every change. |
--mv-progress-color | var(--mv-accent) | Fill color. |
--mv-progress-track | Track color. | |
--mv-progress-segments | 10 | Number of segments (data-variant="segmented"). |
--mv-progress-ring-size | 4rem | Ring diameter. |
--mv-progress-ring-stroke | 3.2 | Stroke width (in units of the 36 viewBox). |
--mv-meter-suboptimum / --mv-meter-bad | Colors of the suboptimal and bad <meter> zones. |
Accessibility
Native <progress> and <meter> expose their roles and values; pair them with a <label for> or aria-label. Ring: role="progressbar", aria-valuenow/min/max (update them along with --value) and aria-valuetext if the label differs. Indeterminate bar: no value, announced as busy. Reduced motion: sweep, shine and spin stop, transitions are instant.