ComponentsProgress
Progress
atomA 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; }
}