Communication
Progress indicators
Linear and circular progress indicators, determinate or indeterminate, flat or with the expressive wave.
- COMPONENT_SPEC §8
- CSS · custom elements · React
- Open in the playground
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.
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.
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.