Spinner
An accessible loading and progress indicator with multiple visual modes, sizes, tones and layout options.
Spinner communicates that work is in progress. It supports indeterminate loading and determinate progress, and can render inline, with a label, over a container, or over the whole viewport.
Basic usage
Without value, Spinner is indeterminate and uses role="status". The default visual is a primary ring.
<Spinner />
<Spinner label="Loading" />
<Spinner aria-label="Checking for updates" />Use a visible label when the state needs to be explained. For an icon-only indicator, provide an explicit aria-label.
Visual variants
The variant prop controls the animated visual:
ring— circular arc with an optional background track; default.dots— three animated dots.bars— four animated vertical bars.pulse— a pulsing circle.
<Spinner variant="ring" label="Loading" />
<Spinner variant="dots" label="Fetching data" />
<Spinner variant="bars" label="Syncing" />
<Spinner variant="pulse" label="Connecting" />track affects the ring variant. Set track={false} to show only the active arc. It has no visual effect on dots, bars or pulse.
<Spinner variant="ring" track={false} aria-label="Loading" />Sizes and tones
size accepts xs, sm, default, lg and xl. The default visual is 22px; the other sizes are 14px, 18px, 30px and 44px.
tone accepts primary, secondary, success, danger, light and inherit.
<Spinner size="xs" tone="secondary" />
<Spinner size="sm" tone="success" />
<Spinner tone="primary" />
<Spinner size="lg" tone="danger" />
<Spinner size="xl" tone="inherit" />
<Spinner tone="light" />Use tone="light" on a dark surface. tone="inherit" takes the current text color from its parent.
Labels and layout
label accepts any ReactNode. The default labelPosition="inline" places it beside the visual; labelPosition="below" stacks it underneath.
<Spinner label="Saving changes" />
<Spinner label={<span>Uploading files…</span>} labelPosition="below" />Determinate progress
Passing value switches Spinner to determinate mode. It then uses role="progressbar" and exposes aria-valuemin, aria-valuemax and aria-valuenow.
The default range is 0..100. min and max define an inclusive custom range, and the rendered value is clamped to that range.
<Spinner value={68} label="Uploading 68%" />
<Spinner value={3} min={0} max={5} label="Step 3 of 5" />
<Spinner value={10} min={10} max={20} label="Starting" />value={0} is determinate and represents the start of the range. A value equal to max is complete; then aria-busy becomes false. While indeterminate, or while determinate progress is below 100%, aria-busy is true.
The current progress is also exposed as the CSS custom property --mm-spinner-progress in percentage form. The other visual variants retain their animation while exposing the same progress accessibility state.
Animation speed and ring thickness
speed is the animation duration in seconds. Values below 0.1 are normalized to 0.1. thickness is the ring border width in pixels; values below 1 are normalized to 1. Thickness is meaningful for variant="ring".
<Spinner speed={1.5} label="Slow background sync" />
<Spinner speed={0.35} label="Fast validation" />
<Spinner variant="ring" thickness={2} />
<Spinner variant="ring" thickness={5} size="lg" />The component respects prefers-reduced-motion by slowing animations automatically.
Container and fullscreen overlays
overlay positions Spinner over the nearest positioned ancestor. The parent should use position: relative (or another positioned layout).
<div style={{ minHeight: 120, position: 'relative' }}>
<Spinner overlay label="Loading panel" />
<PanelContent />
</div>fullscreen creates a fixed viewport overlay for application-level loading. Use it sparingly because it covers the whole interface visually.
<Spinner fullscreen label="Loading application" />Both options work with every variant, size, tone and label position.
Controlled updates
Spinner is presentational: update value from the parent when progress changes. Omitting value keeps it indeterminate.
const [progress, setProgress] = useState(0);
<Spinner value={progress} label={`${progress}% complete`} />Accessibility
- Indeterminate Spinner uses
role="status"and defaults toaria-label="Loading". - Determinate Spinner uses
role="progressbar"with the configured numeric range and bounded value. - A visible
labelis connected witharia-describedby; whenlabelis present, the standalonearia-labelis omitted to avoid duplicate announcements. - Use
aria-labelfor an indicator without a visible label. - The decorative visual is hidden from assistive technology with
aria-hidden="true". - Reduced-motion preferences are respected automatically.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | Loading | Accessible name when no visible label is provided. |
className | string | — | Additional class name on the root element. |
label | ReactNode | — | Visible loading or progress message. |
labelPosition | inline | below | inline | Places the label beside or below the visual. |
variant | ring | dots | bars | pulse | ring | Animated visual treatment. |
size | xs | sm | default | lg | xl | default | Indicator size. |
tone | primary | secondary | success | danger | light | inherit | primary | Theme color applied through currentColor. |
value | number | — | Enables determinate progress and progressbar semantics. |
min | number | 0 | Inclusive lower bound for determinate progress. |
max | number | 100 | Inclusive upper bound for determinate progress. |
track | boolean | true | Shows the inactive ring track; relevant to ring. |
speed | number | 1 | Animation duration in seconds; minimum effective value is 0.1. |
thickness | number | 3 | Ring border width in pixels; minimum effective value is 1. |
overlay | boolean | false | Positions Spinner over the nearest positioned container. |
fullscreen | boolean | false | Positions Spinner as a fixed viewport overlay. |
Related components
- Progress — use when a linear or circular progress track is the primary visualization.
- Button — use its loading state when an action button is waiting.
- Badge — badges can show a compact loading state for async content.
For live examples of every Spinner mode, see the Spinner component showcase.