Communication
Loading indicator
A short wait shown as Material shapes morphing into each other, contained or not, indeterminate or determinate.
- COMPONENT_SPEC §7
- CSS · custom elements · React
- Open in the playground
Examples
Indeterminate
Seven Material shapes morph into each other while the indicator turns. With reduced motion it holds still.
The live demo runs with JavaScript. Its code is below.
import { LoadingIndicator } from '@kyralabs/m3exp-web/react';
export default function Indeterminate() {
return (
<>
<LoadingIndicator aria-label="Loading" />
<LoadingIndicator contained aria-label="Loading" />
</>
);
}
<m3e-loading-indicator aria-label="Loading"></m3e-loading-indicator>
<m3e-loading-indicator contained aria-label="Loading"></m3e-loading-indicator>
Run the HTML version (custom elements, no framework).
Determinate
With progress (0–1) the shape morphs by the value.
The live demo runs with JavaScript. Its code is below.
import { useState } from 'react';
import { LoadingIndicator, Slider } from '@kyralabs/m3exp-web/react';
export default function Determinate() {
const [progress, setProgress] = useState(0.4);
return (
<>
<LoadingIndicator progress={progress} aria-label="Uploading" />
<LoadingIndicator contained progress={progress} aria-label="Uploading" />
<Slider
aria-label="Progress"
minValue={0}
maxValue={1}
value={progress}
onChange={setProgress}
/>
</>
);
}
<m3e-loading-indicator progress="0.4" aria-label="Uploading"></m3e-loading-indicator>
<m3e-loading-indicator contained progress="0.4" aria-label="Uploading"></m3e-loading-indicator>
Run the HTML version (custom elements, no framework).
Your own shapes
Any RoundedPolygons from @kyralabs/m3e-shapes (React only).
The live demo runs with JavaScript. Its code is below.
import { materialShape } from '@kyralabs/m3e-shapes';
import { LoadingIndicator } from '@kyralabs/m3exp-web/react';
const polygons = [materialShape('Heart'), materialShape('Cookie9Sided'), materialShape('Flower')];
export default function Shapes() {
return <LoadingIndicator contained polygons={polygons} aria-label="Loading" />;
}
<!-- Custom shapes are React-only: the element bundles its own m3e-shapes (D7). -->
<m3e-loading-indicator contained aria-label="Loading"></m3e-loading-indicator>
Run the HTML version (custom elements, no framework).
Usage
Short waits (under 5 s); replaces most indeterminate circular progress uses.
API
React
<LoadingIndicator>
Loading indicator (COMPONENT_SPEC §7): the m3e-css .m3e-loading-indicator contract, the same DOM as <m3e-loading-indicator>. Indeterminate: morphs through Compose's seven shapes on its spring and rotates, timed by the --m3e-comp-loading-indicator-* tokens; holds its first frame under prefers-reduced-motion: reduce. Determinate: morphs between its shapes by progress.
| Prop | Type | Default | Description |
|---|---|---|---|
contained
|
boolean
|
Draws the container (Compose ContainedLoadingIndicator).
|
|
progress
|
number
|
0–1 makes it determinate (morphs by progress); omit for indeterminate. | |
polygons
|
readonly RoundedPolygon[]
|
Shapes to morph between (at least two), e.g. materialShape('Heart') from
@kyralabs/m3e-shapes. Defaults to Compose's lists. Keep the array stable across renders.
|
|
aria-label
|
string
|
Accessible name of the progress bar. |
Also accepts HTML attributes not listed here.
RoundedPolygon()
The RoundedPolygon class allows simple construction of polygonal shapes with optional rounding at the vertices. Polygons can be constructed with either the number of vertices desired or an ordered list of vertices.
Custom elements
<m3e-loading-indicator>
import '@kyralabs/m3e-elements/loading-indicator.js';
<m3e-loading-indicator contained progress="0.4" aria-label="Loading"> (COMPONENT_SPEC §7). Renders the m3e-css contract inside itself (the host is display: contents):
``html <div class="m3e-loading-indicator" role="progressbar" aria-label="Loading" data-driven> <svg class="m3e-loading-indicator__shape" viewBox="0 0 100 100" aria-hidden="true" focusable="false" style="rotate: 90deg"><path d="…"></path></svg> </div> ``
Without progress it is indeterminate: it morphs through LoadingIndicatorDefaults.IndeterminateIndicatorPolygons on Compose's spring and rotates, timed by the --m3e-comp-loading-indicator-* choreography tokens. With progress (0–1) it morphs between the determinate polygons by that value. With prefers-reduced-motion: reduce the indeterminate indicator holds its first frame. Custom shapes are React-only (polygons): this element bundles its own m3e-shapes (D7), and graphics-shapes matches corners with instanceof, so polygons built by another copy would not morph. The host's aria-label (or label) names the progress bar and moves onto it.
| Attribute | Property | Type | Notes |
|---|---|---|---|
contained
|
contained
|
boolean
|
|
progress
|
progress
|
number
|
|
label
|
label
|
string
|
|
aria-label
|
hostLabel
|
string
|
CSS
loading-indicator.css
@import '@kyralabs/m3e-css/loading-indicator.css';
Loading indicator: uncontained (default) and contained (COMPONENT_SPEC §7).
<div class="m3e-loading-indicator" role="progressbar" aria-label="Loading">
<svg class="m3e-loading-indicator__shape" viewBox="0 0 100 100" aria-hidden="true">
<path d="…" />
</svg>
</div>
<m3e-loading-indicator> and React <LoadingIndicator> render this DOM and drive the morphing shape (path d + rotation) with JS, marking the root [data-driven]. Without JS (CSS only), whatever static shape the markup carries (e.g. SoftBurst from @kyralabs/m3e-shapes) rotates linearly, one turn per Compose GlobalRotationDuration. Determinate indicators ([aria-valuenow]) never auto-rotate.
7 custom properties it reads
| Custom property | Default |
|---|---|
--m3e-comp-loading-indicator-active-indicator-color
|
var(--m3e-sys-color-primary) |
--m3e-comp-loading-indicator-contained-active-color
|
var(--m3e-sys-color-on-primary-container) |
--m3e-comp-loading-indicator-contained-container-color
|
var(--m3e-sys-color-primary-container) |
--m3e-comp-loading-indicator-container-height
|
48px |
--m3e-comp-loading-indicator-container-shape
|
var(--m3e-sys-shape-corner-full) |
--m3e-comp-loading-indicator-container-width
|
48px |
--m3e-comp-loading-indicator-rotation-duration
|
4666ms |
Accessibility
role="progressbar" aria-label; determinate aria-valuenow/min/max. Not focusable.
shapes scaled uniformly by calculateScaleFactor (max-bounds, so no rotation clips) × 38/48. Reduced motion: the indeterminate indicator holds its first frame and the CSS fallback stops rotating (FOUNDATIONS_SPEC reduced-motion policy: spatial motion snaps; m3.material.io Transitions: "disable decorative effects like … shape morphing"). Compose has no reduced-motion branch at the pin.
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 §7 and the m3e-css sources.
LoadingIndicator.kt, LoadingIndicatorTokens.kt, MaterialShapes.kt.
LoadingIndicatorTokens.kt@19991f2c: container 48 × 48, active size 38, container shape full; uncontained primary; contained primary-container / on-primary-container. Scalable 24–240 keeping ratio (resize the root; the shape is drawn in a square viewBox at ActiveSize / min(ContainerWidth, ContainerHeight)).
loading-indicator.css sources
(compose/material3 @ 19991f2c):
- LoadingIndicatorTokens: container 48 × 48, shape full, active size 38, colors (uncontained primary; contained primary-container / on-primary-container).
- LoadingIndicator.kt: the shape keeps a square aspect ratio, centered in the container (Spacer.aspectRatio(1f, matchHeightConstraintsFirst = true)); the container is clipped to its shape. Resizing the root scales the indicator with the 48 / 38 ratio intact.
- choreography tokens: components/loading-indicator-motion.css.