Components / feedback
Spinner
The delay is CSS rather than a timer in the binding, so a consumer without the React binding gets it too.
Summary
Busy, with no idea how busy.
| APG pattern | none |
| Built on | native elements |
| Status | stable |
| Since | 0.2.0 |
<Spinner delay={300} />
<Spinner label="Publishing" />
Anatomy
A <span data-se="spinner" role="status"> holding screen-reader text. The ring is drawn with a border: the track is --se-border and one edge is --se-accent, so the shape reads as motion at the smallest size.
| Part | Element | Required |
|---|---|---|
sr | <span> | yes |
Properties
| Property | Values | Default | What it does |
|---|---|---|---|
label | string | Loading | What a screen reader hears when this appears. It is real text in the DOM rather than an aria-label, because a live region announces its contents and an empty one announces nothing. |
delay | number | none | Milliseconds to wait before the spinner is visible. Nothing renders during the wait, so a load that finishes first never flickers. 300 is a good default to pass where the work is usually fast. |
States
_None. Rest is the only condition it has._
Behavior
It spins. With a delay, nothing is drawn until the wait has run — the spin starts immediately and the opacity animation holds its from-state for the length of the delay.
Accessibility
The label is text, not an aria-label. A live region announces its contents; an empty one announces nothing. The text sits in data-part="sr", so it is heard and not drawn.
One spinner per wait. A screen full of them announces "Loading" once per region and tells the reader nothing about which part of the page is coming. If several regions load together, put one spinner on the thing they are inside.
Keyboard
None of its own. It is not focusable and takes no keys.
Roles, states and properties
| Attribute | Set by |
|---|---|
role | always status |
Focus
Never takes focus. The announcement is a live region, not a focus move.
Screen reader
Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.
Contrast and target size
npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.
It sets no target size of its own; anything interactive inside it brings its own.
Tested
test/browser/feedback.spec.mjs
Known gaps
_No upstream requirement is recorded as not applicable._
Appearance
| Token | Used for |
|---|---|
--se-accent | border-block-start-color |
--se-border | border |
--se-border-width | border |
--se-duration-instant | animation |
--se-duration-slow | animation |
--se-icon-md | block-size, inline-size |
--se-radius-pill | border-radius |
Motion
One full rotation over twice --se-duration-slow, linear, forever. Plus an opacity animation whose only job is its delay.
prefers-reduced-motion is handled by the reset: it pins the iteration count to one, which stops the spin and leaves a legible ring. The delay survives, because a delay is not motion.
Content
Loading is right for most cases. Name the work when the reader asked for it and is waiting — Publishing, Uploading — because then the announcement confirms what they started.
Usage
Not a Progress. A spinner refuses to say how much is left. The moment you can say "three of ten", a bar is the honest control.
Pass a delay wherever the work is usually fast. A spinner that appears for 200ms reads as a flicker and the page looks broken rather than busy. 300ms is the usual threshold.
When not to use it. For anything the reader triggered and is waiting on — a save, a publish — use the control's own busy state, so the feedback is where their attention already is.
Notes
The delay is CSS rather than a timer in the binding, so a consumer without the React binding gets it too.