Skip to content

Communication

Progress indicators

Linear and circular progress indicators, determinate or indeterminate, flat or with the expressive wave.

Examples

Linear

Determinate with a value (0–1), indeterminate without; shape="wavy" draws the expressive wave.

The live demo runs with JavaScript. Its code is below.

React HTML
import { LinearProgress } from '@kyralabs/m3exp-web/react';

export default function Linear() {
  return (
    <>
      <LinearProgress value={0.4} aria-label="Loading news article" />
      <LinearProgress aria-label="Loading my episodes" />
      <LinearProgress shape="wavy" value={0.6} aria-label="Downloading" />
      <LinearProgress shape="wavy" aria-label="Syncing" />
    </>
  );
}
<m3e-linear-progress value="0.4" aria-label="Loading news article"></m3e-linear-progress>
<m3e-linear-progress aria-label="Loading my episodes"></m3e-linear-progress>
<m3e-linear-progress shape="wavy" value="0.6" aria-label="Downloading"></m3e-linear-progress>
<m3e-linear-progress shape="wavy" aria-label="Syncing"></m3e-linear-progress>

Run the HTML version (custom elements, no framework).

Circular

The same options; thickness="thick" is the 8dp configuration.

The live demo runs with JavaScript. Its code is below.

React HTML
import { CircularProgress } from '@kyralabs/m3exp-web/react';

export default function Circular() {
  return (
    <>
      <CircularProgress value={0.4} aria-label="Uploading" />
      <CircularProgress aria-label="Loading" />
      <CircularProgress shape="wavy" value={0.7} aria-label="Downloading" />
      <CircularProgress shape="wavy" thickness="thick" aria-label="Syncing" />
    </>
  );
}
<m3e-circular-progress value="0.4" aria-label="Uploading"></m3e-circular-progress>
<m3e-circular-progress aria-label="Loading"></m3e-circular-progress>
<m3e-circular-progress shape="wavy" value="0.7" aria-label="Downloading"></m3e-circular-progress>
<m3e-circular-progress shape="wavy" thickness="thick" aria-label="Syncing"></m3e-circular-progress>

Run the HTML version (custom elements, no framework).

API

React

<LinearProgress>

Linear progress indicator (COMPONENT_SPEC §8): the m3e-css .m3e-linear-progress contract, the same DOM as <m3e-linear-progress>, driven by m3e-elements' LinearProgressDriver (value on Compose's progress spring with velocity kept, wavy paths every frame).

Prop Type Default Description
value number 0–1 makes it determinate; omit for indeterminate.
shape 'flat' | 'wavy' flat wavy draws Compose's wave (Linear/CircularWavyProgressIndicator). Default flat.
thickness 'default' | 'thick' thick: the 8dp sample configuration (14dp wavy linear, 52dp circular containers).
amplitude number | ((progress: number) => number) Wave amplitude, 0–1 (Compose amplitude): fixed, or a function of the progress. Default: flat at ≤ 10% and ≥ 95% (determinate), full (indeterminate).
wavelength number Wavelength in px (Compose wavelength). Default: the wavelength token.
waveSpeed number Wave speed in px per second (Compose waveSpeed). Default: one wavelength per second.
aria-label string Accessible name of the progress bar (e.g. "Loading news article").

Also accepts HTML attributes not listed here.

<CircularProgress>

Circular progress indicator (COMPONENT_SPEC §8): the m3e-css .m3e-circular-progress contract, the same DOM as <m3e-circular-progress>, driven by m3e-elements' CircularProgressDriver (value on Compose's progress spring, the wavy star every frame).

Prop Type Default Description
value number 0–1 makes it determinate; omit for indeterminate.
shape 'flat' | 'wavy' flat wavy draws Compose's wave (Linear/CircularWavyProgressIndicator). Default flat.
thickness 'default' | 'thick' thick: the 8dp sample configuration (14dp wavy linear, 52dp circular containers).
amplitude number | ((progress: number) => number) Wave amplitude, 0–1 (Compose amplitude): fixed, or a function of the progress. Default: flat at ≤ 10% and ≥ 95% (determinate), full (indeterminate).
wavelength number Wavelength in px (Compose wavelength). Default: the wavelength token.
waveSpeed number Wave speed in px per second (Compose waveSpeed). Default: one wavelength per second.
aria-label string Accessible name of the progress bar (e.g. "Loading news article").

Also accepts HTML attributes not listed here.

Custom elements

<m3e-linear-progress>

import '@kyralabs/m3e-elements/linear-progress.js';

<m3e-linear-progress value="0.4" shape="wavy" aria-label="Loading"> (COMPONENT_SPEC §8). Renders the m3e-css .m3e-linear-progress contract inside itself (the host is display: contents):

``html <div class="m3e-linear-progress" role="progressbar" aria-label="Loading" aria-valuemin="0" aria-valuemax="100" aria-valuenow="40" data-driven style="--m3e-comp-progress-indicator-value: 0.4"> <span class="m3e-linear-progress__track"></span> <span class="m3e-linear-progress__indicator"></span> <span class="m3e-linear-progress__stop"></span> </div> ``

Without value it is indeterminate. shape="wavy" draws Compose's wave into an SVG every frame; thickness="thick" is the 8dp sample configuration. Value changes run on Compose's progress spring (velocity kept when interrupted). amplitude, wavelength and wave-speed override the wave (Compose amplitude, wavelength, waveSpeed).

Attribute Property Type Notes
value value number
shape shape string
thickness thickness string
amplitude amplitude number
wavelength wavelength number
wave-speed waveSpeed number
label label string
aria-label hostLabel string

<m3e-circular-progress>

import '@kyralabs/m3e-elements/circular-progress.js';

<m3e-circular-progress value="0.4" shape="wavy" aria-label="Downloading"> (COMPONENT_SPEC §8). Renders the m3e-css .m3e-circular-progress contract inside itself (the host is display: contents):

``html <div class="m3e-circular-progress" role="progressbar" aria-label="Downloading" aria-valuemin="0" aria-valuemax="100" aria-valuenow="40" data-driven style="--m3e-comp-progress-indicator-value: 0.4"> <svg class="m3e-circular-progress__svg" aria-hidden="true" focusable="false"> <circle class="m3e-circular-progress__track"></circle> <circle class="m3e-circular-progress__indicator"></circle> </svg> </div> ``

Without value it is indeterminate. shape="wavy" draws Compose's wavy star (bundled m3e-shapes, D7) into <path>s every frame; thickness="thick" is the 8dp / 52dp sample configuration. Value changes run on Compose's progress spring (velocity kept when interrupted). amplitude, wavelength and wave-speed override the wave.

Attribute Property Type Notes
value value number
shape shape string
thickness thickness string
amplitude amplitude number
wavelength wavelength number
wave-speed waveSpeed number
label label string
aria-label hostLabel string

CSS

linear-progress.css

@import '@kyralabs/m3e-css/linear-progress.css';

Linear progress indicator: flat (CSS only) and wavy (drawn by <m3e-linear-progress> / m3e-react <LinearProgress>), determinate and indeterminate (COMPONENT_SPEC §8).

Determinate ([aria-valuenow]; the value, 0–1, is --m3e-comp-progress-indicator-value):

<div class="m3e-linear-progress" role="progressbar" aria-label="Loading news article"
     aria-valuemin="0" aria-valuemax="100" aria-valuenow="40"
     style="--m3e-comp-progress-indicator-value: 0.4">
  <span class="m3e-linear-progress__track"></span>
  <span class="m3e-linear-progress__indicator"></span>
  <span class="m3e-linear-progress__stop"></span>
</div>

Indeterminate (no aria-valuenow): the parts in drawing order — track, line, track, line, track:

<div class="m3e-linear-progress" role="progressbar" aria-label="Loading my episodes">
  <span class="m3e-linear-progress__track"></span>
  <span class="m3e-linear-progress__indicator"></span>
  <span class="m3e-linear-progress__track"></span>
  <span class="m3e-linear-progress__indicator"></span>
  <span class="m3e-linear-progress__track"></span>
</div>

Wavy (--wavy): the root holds an <svg class="m3e-linear-progress__wave"> whose paths (__wave-track, __wave-indicator, and the __wave-stop circle) the element / React component draw every frame; without JS it shows nothing. Modifiers: --wavy, --thick. JS layers set [data-driven] and animate the value themselves (Compose's spring, velocity kept); without it a value change transitions on the same spring as a linear() curve (no velocity carry-over).

22 custom properties it reads
Custom property Default
--m3e-comp-linear-progress-indicator-active-thickness 4px
--m3e-comp-linear-progress-indicator-active-wave-wavelength 40px
--m3e-comp-linear-progress-indicator-height 4px
--m3e-comp-linear-progress-indicator-stop-size 4px
--m3e-comp-linear-progress-indicator-track-active-space 4px
--m3e-comp-linear-progress-indicator-track-thickness 4px
--m3e-comp-linear-progress-indicator-wave-height 10px
--m3e-comp-progress-indicator-active-indicator-color var(--m3e-sys-color-primary)
--m3e-comp-progress-indicator-active-shape var(--m3e-sys-shape-corner-full)
--m3e-comp-progress-indicator-defaults-linear-indeterminate-duration 1750ms
--m3e-comp-progress-indicator-defaults-linear-stop-trailing-space-max 6px
--m3e-comp-progress-indicator-defaults-linear-thick-height 8px
--m3e-comp-progress-indicator-defaults-linear-thick-wavy-height 14px
--m3e-comp-progress-indicator-defaults-linear-width 240px
--m3e-comp-progress-indicator-defaults-progress-duration 1305ms
--m3e-comp-progress-indicator-defaults-progress-easing linear(0, 0.0012 0.54%, 0.0052 1.15%, 0.0119 1.76%, 0.0208 2.38%, 0.0317 2.99%, 0.0461 3.68%, 0.0624 4.37%, 0.0824 5.13%, 0.1176 6.36%, 0.1607 7.74%, 0.366 13.87%, 0.4179 15.48%, 0.4629 16.93%, 0.5124 18.62%, 0.5567 20.23%, 0.5981 21.84%, 0.6365 23.45%, 0.672 25.06%, 0.706 26.74%, 0.7371 28.43%, 0.7665 30.19%, 0.793 31.95%, 0.8178 33.79%, 0.8399 35.63%, 0.8603 37.55%, 0.8804 39.69%, 0.8988 41.99%, 0.9151 44.37%, 0.9297 46.9%, 0.9422 49.5%, 0.9534 52.34%, 0.9632 55.4%, 0.9713 58.62%, 0.9778 61.92%, 0.9833 65.52%, 0.9914 73.79%, 0.996 83.14%, 0.999 99.92%, 1)
--m3e-comp-progress-indicator-defaults-thick-thickness 8px
--m3e-comp-progress-indicator-stop-color var(--m3e-sys-color-primary)
--m3e-comp-progress-indicator-stop-shape var(--m3e-sys-shape-corner-full)
--m3e-comp-progress-indicator-track-color var(--m3e-sys-color-secondary-container)
--m3e-comp-progress-indicator-track-shape var(--m3e-sys-shape-corner-full)
--m3e-comp-progress-indicator-value set by the component

circular-progress.css

@import '@kyralabs/m3e-css/circular-progress.css';

Circular progress indicator: flat (CSS only) and wavy (drawn by <m3e-circular-progress> / m3e-react <CircularProgress>), determinate and indeterminate (COMPONENT_SPEC §8).

<div class="m3e-circular-progress" role="progressbar" aria-label="Downloading"
     aria-valuemin="0" aria-valuemax="100" aria-valuenow="40"
     style="--m3e-comp-progress-indicator-value: 0.4">
  <svg class="m3e-circular-progress__svg" aria-hidden="true" focusable="false">
    <circle class="m3e-circular-progress__track"></circle>
    <circle class="m3e-circular-progress__indicator"></circle>
  </svg>
</div>

Indeterminate: the same markup without aria-valuenow (and without the value). Wavy (--wavy): the parts are <path>s the element / React component draw every frame (no geometry from CSS); without JS it shows nothing. Modifiers: --wavy, --thick. JS layers set [data-driven] and animate the value themselves (Compose's spring, velocity kept); without it a value change transitions on the same spring as a linear() curve (no velocity carry-over).

15 custom properties it reads
Custom property Default
--m3e-comp-circular-progress-indicator-active-thickness 4px
--m3e-comp-circular-progress-indicator-active-wave-wavelength 15px
--m3e-comp-circular-progress-indicator-size 40px
--m3e-comp-circular-progress-indicator-track-active-space 4px
--m3e-comp-circular-progress-indicator-track-thickness 4px
--m3e-comp-circular-progress-indicator-wave-size 48px
--m3e-comp-progress-indicator-active-indicator-color var(--m3e-sys-color-primary)
--m3e-comp-progress-indicator-defaults-circular-indeterminate-duration 6000ms
--m3e-comp-progress-indicator-defaults-circular-indeterminate-track-color transparent
--m3e-comp-progress-indicator-defaults-circular-thick-size 52px
--m3e-comp-progress-indicator-defaults-progress-duration 1305ms
--m3e-comp-progress-indicator-defaults-progress-easing linear(0, 0.0012 0.54%, 0.0052 1.15%, 0.0119 1.76%, 0.0208 2.38%, 0.0317 2.99%, 0.0461 3.68%, 0.0624 4.37%, 0.0824 5.13%, 0.1176 6.36%, 0.1607 7.74%, 0.366 13.87%, 0.4179 15.48%, 0.4629 16.93%, 0.5124 18.62%, 0.5567 20.23%, 0.5981 21.84%, 0.6365 23.45%, 0.672 25.06%, 0.706 26.74%, 0.7371 28.43%, 0.7665 30.19%, 0.793 31.95%, 0.8178 33.79%, 0.8399 35.63%, 0.8603 37.55%, 0.8804 39.69%, 0.8988 41.99%, 0.9151 44.37%, 0.9297 46.9%, 0.9422 49.5%, 0.9534 52.34%, 0.9632 55.4%, 0.9713 58.62%, 0.9778 61.92%, 0.9833 65.52%, 0.9914 73.79%, 0.996 83.14%, 0.999 99.92%, 1)
--m3e-comp-progress-indicator-defaults-thick-thickness 8px
--m3e-comp-progress-indicator-track-color var(--m3e-sys-color-secondary-container)
--m3e-comp-progress-indicator-value set by the component

Accessibility

role="progressbar" with an accessible name (aria-label / label); determinate aria-valuemin="0" aria-valuemax="100" aria-valuenow (rounded percent). Not focusable.

gap = min(progress length, gap). At 0% the flat linear and flat circular indicators draw no indicator (zero sweep); the wavy circular one draws a dot (zero-length segment with round caps), the wavy linear one none. Near 100% the flat circular track's sweep turns negative and, as Skia draws negative sweeps, runs backwards from its start (fills the closing gap), as Compose draws it on Android: drawArc reaches Skia with the sweep unchanged (register #64). Fidelity: PathMeasure positions differ sub-pixel (Skia measures curves to 0.5px, m3e-shapes with 32 samples per curve), so wavy segments may end a fraction of a pixel apart. RTL: linear indicators mirror (wavy ones rotate 180° like Compose); circular ones do not (m3.material.io). Reduced motion (project choice, FOUNDATIONS §4): the decorative wave holds still, value changes and amplitude tweens jump (spatial), indeterminate lines and arcs keep moving: the motion is the only cue that the wait goes on (WCAG 2.2.2 treats such motion as essential), unlike the loading indicator's decorative morph. Forced colors: indicator and stop CanvasText, track GrayText.

Every interactive part has a target of at least 48 × 48 px, a visible focus ring for keyboard focus only, and works under forced-colors and prefers-reduced-motion.

Sources

Values come from androidx Compose Material3 1.5.0-beta01 at 19991f2c and from m3.material.io, as cited below from COMPONENT_SPEC §8 and the m3e-css sources.

ProgressIndicator.kt, WavyProgressIndicator.kt, internal/LinearWavyProgressModifiers.kt, internal/CircularWavyProgressModifiers.kt, ProgressIndicatorSamples.kt, *ProgressIndicatorTokens.kt, MotionTokens.kt (vendored); animation-core AnimationSpec.kt (keyframes using), VectorizedAnimationSpec.kt (Spring constants); m3.material.io Progress indicators (overview, specs, guidelines, accessibility; token DB thick values).

{Linear,Circular}ProgressIndicatorTokens.kt, ProgressIndicatorTokens.kt@19991f2c plus the ProgressIndicatorDefaults / WavyProgressIndicatorDefaults values Compose renders (components/progress-indicator-defaults.css; SOURCES register #26). Corrects the earlier MDC-Android / secondary values:

Colors: indicator and stop primary, track secondary-container; the flat indeterminate circular indicator has no track (circularIndeterminateTrackColor = Transparent), the wavy one keeps it.

linear-progress.css sources

(compose/material3 @ 19991f2c; generated values in linear-progress-indicator.css, progress-indicator.css, progress-indicator-defaults.css; lines in PARITY.md):

  • ProgressIndicator.kt LinearProgressIndicator (L138–335): 240dp × Height (4dp) by default; the stroke is the height with round caps, every part's ends clamped to [cap, width − cap] (cap = half the stroke) and drawn only when it has length (drawLinearIndicator, L397–434). The track starts at progress + min(progress, gap + stroke) (the gap measured between the caps), so gap = min(progress length, TrackActiveSpace). Stop indicator (drawStopIndicator, L879–926): a min(StopSize, height) dot at min((height − size) / 2, 6dp) from the end, drawn over the rest. Indeterminate: track before line 1, line 1, track between, line 2, track after line 2, each with its condition (L294–333); the four head / tail values run Compose's keyframes (@keyframes m3e-progress-* in progress-indicator-defaults.css). RTL mirrors (logical insets).
  • Wavy: WavyProgressIndicatorDefaults.LinearContainerHeight = WaveHeight (10dp); the element draws LinearWavyProgressModifiers.kt (m3e-shapes LinearWavyPathCache) and rotates it 180° in RTL like Compose.
  • Thick: 8dp strokes; flat height 8dp (m3.material.io token DB), wavy container 14dp (LinearThickWavyProgressIndicatorSample).
  • Forced colors: the indicator and stop draw in CanvasText, the track in GrayText.
circular-progress.css sources

(compose/material3 @ 19991f2c; generated values in circular-progress-indicator.css, progress-indicator.css, progress-indicator-defaults.css; lines in PARITY.md):

  • ProgressIndicator.kt CircularProgressIndicator (L516–676): Size (40dp) square; arcs of diameter size − stroke with round caps, from 12 o'clock clockwise. The track starts min(sweep, gap) after the progress and sweeps 360° − sweep − 2 × min(sweep, gap), where the gap angle is (gap + stroke) / (π × size) of a turn; near 100% that sweep turns negative and, as Skia draws negative sweeps, runs backwards from its start. A zero sweep draws nothing. Both as Compose draws them on Android: drawArc reaches Skia with the sweep unchanged (SOURCES #64). Indeterminate: no track (circularIndeterminateTrackColor = Transparent), the arc starts at 3 o'clock and the whole drawing turns by the global + additional rotation, its sweep animated (@keyframes m3e-progress-* in progress-indicator-defaults.css).
  • Wavy: WavyProgressIndicatorDefaults.CircularContainerSize = WaveSize (48dp); the element draws CircularWavyProgressModifiers.kt (m3e-shapes CircularWavyPathCache).
  • Thick: 8dp strokes in a 52dp box (CircularThickWavyProgressIndicatorSample; the m3.material.io token DB's circular thick size, used for flat ones too).
  • Circular indicators are not mirrored in RTL (m3.material.io › Guidelines › Right-to-left).
  • Forced colors: the indicator draws in CanvasText, the track in GrayText.