Navigation

v0.0.4
Feedback

Progress

A thin well that fills with ink as work completes.

On this page

Progress shows how much of a task is done. Use .progress on a native <progress>: the browser reports the value to assistive technology, and the class draws a thin well that fills with ink. For work of unknown length inside a control, use a spinner.

Progress

<label class="field">Uploading 3 of 5 files  <progress class="progress" value="62" max="100">62%</progress></label>

#Indeterminate

Leave out value while the amount is unknown. A band of ink travels along the track; with reduced motion the track rests at an even half tone instead.

Indeterminate

<label class="field">Preparing the build  <progress class="progress"></progress></label>

#Sizes

The track is 6px thick. progress-sm is 4px, for a row in a list, and progress-lg is 8px.

Sizes

<progress class="progress progress-sm" value="30" max="100" aria-label="Small">30%</progress><progress class="progress" value="55" max="100" aria-label="Medium">55%</progress><progress class="progress progress-lg" value="80" max="100" aria-label="Large">80%</progress>

#Segmented

progress-segmented cuts the track into equal pieces, for progress that is a count of things: three of five steps, four of eight uploads. Set --progress-steps to the same number as max, so a piece is a step. The cut is a mask over the well and its fill alike, which keeps the pieces in step with the value.

Segmented

<label class="field">Step 3 of 5  <progress class="progress progress-segmented" max="5" value="3">Step 3 of 5</progress></label><label class="field">5 of 8 files uploaded  <progress class="progress progress-segmented progress-lg" style="--progress-steps: 8" max="8" value="5">5 of 8</progress></label>

#Ring

progress-ring draws the same amount around a circle, for a tile, a row or the corner of a card. It is an element with role="progressbar". Where the browser supports typed attr(), the ring reads aria-valuenow and aria-valuemax itself, so the amount is written once, for people and for the drawing, and the arc eases to each new value. Elsewhere, set --progress-ring-value in a style attribute as well.

The element's own content is the label in the middle. Both ends of the arc are round, and an empty ring is an empty track.

Ring

<div class="progress-ring progress-ring-sm" role="progressbar" aria-label="Upload" aria-valuenow="40"></div><div class="progress-ring" role="progressbar" aria-label="Storage used" aria-valuenow="64">64</div><div class="progress-ring progress-ring-lg" role="progressbar" aria-label="Steps done" aria-valuenow="3" aria-valuemax="8" aria-valuetext="3 of 8">3/8</div><div class="progress-ring progress-ring-xl" role="progressbar" aria-label="Coverage" aria-valuenow="86">86%</div>

Without aria-valuenow the amount is unknown, and a quarter arc travels the track. Set --progress-ring-color to a status color when the ring reports one, and say the status in words beside it.

Ring states

<div class="progress-ring" role="progressbar" aria-label="Preparing"></div><div class="progress-ring progress-ring-lg" role="progressbar" aria-label="Checks passed" aria-valuenow="100" style="--progress-ring-color: var(--color-success)">100</div><div class="progress-ring progress-ring-lg" role="progressbar" aria-label="Quota used" aria-valuenow="92" style="--progress-ring-color: var(--color-danger)">92</div>

#With a value

The bar does not print its number. Put it beside the label in tabular figures so it does not shift as it counts.

With a value

<div class="flex flex-col gap-xs">  <div class="flex items-baseline justify-between text-sm">    <span class="fg-text-strong" id="storage-label">Storage</span>    <span class="fg-text-muted">7.4 of 10 GB</span>  </div>  <progress class="progress" value="7.4" max="10" aria-labelledby="storage-label">74%</progress></div>

#Accessibility

  • Label the bar with a field label, aria-label or aria-labelledby.
  • The text inside <progress> is a fallback for browsers that do not draw the element.
  • When the bar describes a region that is loading, set aria-busy="true" on that region until it is done.

#Classes

ClassDescription
progressA progress bar, on a native progress element with a label: a 6px track that fills with ink and eases to each new value. The text inside the element is a fallback
progress-smA 4px track, for a row in a list; the default is 6px. With progress
progress-lgAn 8px track; the default is 6px. With progress
progress-segmentedThe track in --progress-steps equal pieces, five by default, for a count of things. Set --progress-steps to the same number as max. With progress
progress-ringA ring, 40px across, on an element with role="progressbar", an accessible name and the amount in aria-valuenow. The element's own content is the label in the middle
progress-ring-smA 20px ring; the default is 40px. With progress-ring
progress-ring-lgA 64px ring; the default is 40px. With progress-ring
progress-ring-xlA 96px ring with a larger figure in the middle; the default is 40px. With progress-ring

#Custom properties

PropertyDefaultDescription
--progress-height.375remTrack thickness. On progress
--progress-colorvar(--color-surface-inverse)Fill color. On progress
--progress-steps5Number of pieces of a segmented track; set it to the same number as max. On progress-segmented
--progress-ring-valueThe ring's amount. It comes from aria-valuenow where typed attr() is supported; set it by hand elsewhere. On progress-ring
--progress-ring-max100The amount of a full ring. It comes from aria-valuemax where typed attr() is supported; set it by hand elsewhere. On progress-ring
--progress-ring-size2.5remThe ring's diameter. On progress-ring
--progress-ring-widthmax(2px, calc(var(--progress-ring-size) / 10))The ring's thickness: a tenth of the size, and 2px at least. On progress-ring
--progress-ring-colorvar(--color-surface-inverse)The arc's color; set it to a status color when the ring reports one. On progress-ring

#Source

@master/ui/components/progress.css
/* * Progress: a native <progress> drawn as a thin well that fills with ink. * Without a value it is indeterminate: a band of ink travels along the track. * A ring draws the same amount around a circle, for a tile, a row or the corner of a card. */@property --progress-ring-value {  syntax: '<number>';  inherits: true;  initial-value: 0;}@layer components {  .progress {    --progress-height: .375rem;    --progress-color: var(--color-surface-inverse);    display: block;    width: 100%;    height: var(--progress-height);    margin: 0;    border: 0;    border-radius: 999px;    overflow: hidden;    background: var(--color-surface-inset);    box-shadow: var(--shadow-pressed);    vertical-align: middle;    appearance: none;    &::-webkit-progress-bar {      background: transparent;    }    /* The fill keeps a round end and eases to each new value. */    &::-webkit-progress-value {      border-radius: 999px;      background: var(--progress-color);      transition: inline-size var(--duration-base) var(--easing-glide);    }    &::-moz-progress-bar {      border-radius: 999px;      background: var(--progress-color);    }    &:indeterminate {      background:        linear-gradient(90deg, transparent, var(--progress-color) 30%, var(--progress-color) 70%, transparent) 0 0 / 40% 100% no-repeat,        var(--color-surface-inset);      /* Linear: an eased loop would rest at its ends, where the band is outside the track. */      animation: progress-travel 1.2s linear infinite;    }    &:indeterminate::-webkit-progress-value {      background: transparent;    }    &:indeterminate::-moz-progress-bar {      background: transparent;    }    &.progress-sm { --progress-height: .25rem; }    &.progress-lg { --progress-height: .5rem; }    /*     * Segmented: the track is cut into --progress-steps equal pieces, for a count of things: four of five     * steps, three of four checks. The cut is a mask over the well and its fill alike, so the pieces stay     * in step with each other; their ends are square and the track's own corner is small to match.     */    &.progress-segmented {      --progress-steps: 5;      --progress-gap: 3px;      border-radius: 1.5px;      /* A piece that is still empty is counted too, so the well takes a tint of the fill and holds on any surface. */      background: color-mix(in oklab, var(--progress-color) 20%, var(--color-surface-inset));      mask: linear-gradient(90deg, black calc(100% - var(--progress-gap)), transparent 0) 0 0 / calc((100% + var(--progress-gap)) / var(--progress-steps)) 100%;      &::-webkit-progress-value {        border-radius: 0;      }      &::-moz-progress-bar {        border-radius: 0;      }    }    @media (prefers-reduced-motion: reduce) {      &::-webkit-progress-value {        transition: none;      }      /* Without travel, an even half tone says the amount is unknown. */      &:indeterminate {        background: color-mix(in oklab, var(--progress-color) 30%, var(--color-surface-inset));        animation: none;      }    }    @media (forced-colors: active) {      --progress-color: Highlight;      outline: 1px solid CanvasText;      outline-offset: -1px;      forced-color-adjust: none;      background: Canvas;    }  }  /*   * Ring: an element with role="progressbar". Where the browser supports typed attr(), the ring reads   * aria-valuenow and aria-valuemax itself, so the amount is written once, for people and for the drawing;   * elsewhere set --progress-ring-value. Both ends of the arc are round, and the arc eases to each new value.   * Without aria-valuenow the amount is unknown: a quarter arc travels the track.   * The element's own content is the label in the middle.   */  .progress-ring {    --progress-ring-size: 2.5rem;    --progress-ring-width: max(2px, calc(var(--progress-ring-size) / 10));    --progress-ring-color: var(--color-surface-inverse);    --progress-ring-track: color-mix(in oklab, var(--progress-ring-color) 11%, transparent);    --progress-ring-max: 100;    --progress-ring-turn: calc(var(--progress-ring-value) / var(--progress-ring-max));    position: relative;    display: inline-grid;    flex: none;    place-items: center;    width: var(--progress-ring-size);    height: var(--progress-ring-size);    color: var(--color-text-strong);    font: var(--font-weight-control) calc(var(--progress-ring-size) * .27)/1 var(--font-family-sans);    font-variant-numeric: tabular-nums;    letter-spacing: -.01em;    vertical-align: middle;    transition: --progress-ring-value var(--duration-base) var(--easing-glide);    @supports (x: attr(x type(*))) {      &[aria-valuenow] {        --progress-ring-value: attr(aria-valuenow type(<number>), 0);      }      &[aria-valuemax] {        --progress-ring-max: attr(aria-valuemax type(<number>), 100);      }    }    /*     * The drawing: an arc from a conic gradient, a dot at each end to round it, and a mask that keeps the ring.     * The dots have no radius at zero, so an empty ring is an empty track.     */    &::before {      --progress-ring-radius: calc((var(--progress-ring-size) - var(--progress-ring-width)) / 2);      --progress-ring-cap: calc(var(--progress-ring-width) / 2 * min(1, var(--progress-ring-turn) * 200));      content: '';      position: absolute;      inset: 0;      border-radius: 999px;      background:        radial-gradient(circle at 50% calc(var(--progress-ring-width) / 2), var(--progress-ring-color) calc(var(--progress-ring-cap) - .5px), transparent var(--progress-ring-cap)),        radial-gradient(circle at calc(50% + var(--progress-ring-radius) * sin(var(--progress-ring-turn) * 360deg)) calc(50% - var(--progress-ring-radius) * cos(var(--progress-ring-turn) * 360deg)), var(--progress-ring-color) calc(var(--progress-ring-cap) - .5px), transparent var(--progress-ring-cap)),        conic-gradient(var(--progress-ring-color) calc(var(--progress-ring-turn) * 360deg), var(--progress-ring-track) 0);      mask: radial-gradient(farthest-side, transparent calc(100% - var(--progress-ring-width) - .5px), black calc(100% - var(--progress-ring-width)), black calc(100% - .5px), transparent);    }    &:not([aria-valuenow]) {      --progress-ring-value: 25;      --progress-ring-max: 100;      &::before {        animation: progress-ring-travel .9s linear infinite;      }    }    &.progress-ring-sm {      --progress-ring-size: 1.25rem;      --progress-ring-width: 2.5px;    }    &.progress-ring-lg {      --progress-ring-size: 4rem;      --progress-ring-width: 5px;    }    &.progress-ring-xl {      --progress-ring-size: 6rem;      --progress-ring-width: 6px;      font-size: 1.375rem;      letter-spacing: -.02em;    }    @media (prefers-reduced-motion: reduce) {      transition: none;      &:not([aria-valuenow])::before {        animation-duration: 2.4s;      }    }    @media (forced-colors: active) {      --progress-ring-color: Highlight;      --progress-ring-track: GrayText;      forced-color-adjust: none;    }  }}@keyframes progress-ring-travel {  to {    rotate: 360deg;  }}@keyframes progress-travel {  from {    background-position: -67% 0, 0 0;  }  to {    background-position: 167% 0, 0 0;  }}