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.

CategoryFeedback
TypeCSS only (.mv-progress)
Statusstable
Keywordsprogress, progression, progressbar, ring, circular, meter, jauge, gauge, loading, upload

When to use

Avoid when

Install

node scripts/add.mjs progress --out ./src/marvelous

AI 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

NameTypeDefaultDescription
data-sizexs | sm | lg(md)Bar thickness or ring size (sm | lg).
data-colorsuccess | warning | danger | info | neutral(accent)Fill color.
data-shinebooleanShine that runs across an in-progress determinate bar.
data-variantsegmentedSplits the bar / gauge into segments.
data-indeterminatebooleanOn .mv-progress-ring: continuously spinning arc.

CSS classes

NameDescription
mv-progressOn <progress value max>; without value = indeterminate (sweep).
mv-meterOn <meter min max low high optimum value>: accent when optimal, then warning and danger based on the native thresholds.
mv-progress-ringRing: 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 / -barTrack and value circles of the ring.
mv-progress-ring-labelCenter label; empty = automatically animated percentage (CSS counter).
mv-progress-fieldLabel + value grid above the bar, hint text below.
mv-progress-label / -value / -hintField parts.

CSS variables

NameDefaultDescription
--value0Ring value (0 to 100), with a smooth transition and animated counter on every change.
--mv-progress-colorvar(--mv-accent)Fill color.
--mv-progress-trackTrack color.
--mv-progress-segments10Number of segments (data-variant="segmented").
--mv-progress-ring-size4remRing diameter.
--mv-progress-ring-stroke3.2Stroke width (in units of the 36 viewBox).
--mv-meter-suboptimum / --mv-meter-badColors 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.