ComponentsProgress

Progress

atom

A determinate progress bar, heavier than the slider to read as ongoing work.

Storage
Media library7.4 of 10 GB
74% used — upgrade for more space.
Syncing… (indeterminate — no ETA)
Generating… (wavy — the expressive hero moment)

Recipe CSS

This is the exact CSS your kit ships for Progress — token-driven, so it re-themes with every knob. Get it (and the rest) from the configurator's Use kit panel.

/* === Progress ===
 * Heavier than the slider — stroke-progress (6px) signals "ongoing work"
 * rather than "controllable value". The 2:1 thickness ratio (6 vs 3) is
 * intentional: a glance distinguishes them without reading the label. */
.progress {
  width: 100%;
  height: var(--k-stroke-progress, 6px);
  background: var(--k-surface-2);
  border-radius: 999px;
  overflow: hidden;
}
.progress__fill {
  height: 100%;
  background: var(--k-fill, var(--k-primary));
  border-radius: 999px;
}
/* Indeterminate — no known %: a 40%-wide fill sweeps the track on a loop. Use
   when there's no ETA (Radix Progress data-state="indeterminate"); the markup
   drops aria-valuenow and keeps role=progressbar. Respects reduced-motion. */
.progress--indeterminate .progress__fill {
  width: 40%;
  animation: k-progress-indet 1.4s var(--k-ease, ease) infinite;
}
@keyframes k-progress-indet {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(250%); }
}
@media (prefers-reduced-motion: reduce) {
  .progress--indeterminate .progress__fill { animation: none; width: 100%; opacity: 0.6; }
}
/* Wavy progress (H4 flourish) — M3-Expressive's wavy indicator, CSS-only.
   The FILL is masked by a tiling sine stroke (the path's end slopes match its
   start, so the 24px tile repeats seamlessly) and slides its mask one tile
   per loop for the ripple; the REMAINING track is painted by the root as a
   thin line from the right. Plain width:% can't tell the root where the fill
   ends, so the wavy variant takes its value as a custom prop instead:
   <div class="progress progress--wavy" style="--progress: 64%"> — use it for
   the one hero progress moment (an upload, a generation), not every meter. */
.progress--wavy {
  height: 14px;
  border-radius: 999px;
  background:
    linear-gradient(var(--k-surface-2) 0 0)
    right center / calc(100% - var(--progress, 0%)) var(--k-stroke-progress, 6px)
    no-repeat;
}
.progress--wavy .progress__fill {
  width: var(--progress, 0%);
  border-radius: 0;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 14'%3E%3Cpath d='M0 7 Q6 1 12 7 T24 7' fill='none' stroke='%23fff' stroke-width='5' stroke-linecap='round'/%3E%3C/svg%3E") 0 0 / 24px 100% repeat-x;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 14'%3E%3Cpath d='M0 7 Q6 1 12 7 T24 7' fill='none' stroke='%23fff' stroke-width='5' stroke-linecap='round'/%3E%3C/svg%3E") 0 0 / 24px 100% repeat-x;
  animation: k-progress-wave 1.1s linear infinite;
}
@keyframes k-progress-wave {
  to { -webkit-mask-position: 24px 0; mask-position: 24px 0; }
}
@media (prefers-reduced-motion: reduce) {
  .progress--wavy .progress__fill { animation: none; }
}