Skip to contentAsheeUIAsheeUI

Spinner

A small animated ring that says something is happening, and nothing else.

A spinner says one thing: something is happening. It has no progress, no estimate and no words, which is why it is small, and why it is the right answer only for a wait short enough that the reader does not need to know more than "still working".

It is decorative, and the component states that in its markup: the SVG carries aria-hidden="true". So a spinner never announces anything itself. Inside a control it is described by the control; on its own it needs a wrapper that carries the status and the words. A skeleton says the shape of what is coming; a loading-state claims the region; a spinner is the busy mark that fits in a button, a row or beside a line of text.

Usage

On its own, the spinner is only half the answer, the wrapper is the other half:

import { Spinner } from "@asheeui/web";

<div role="status">
  <Spinner />
  <span className="sr-only">Loading the invoice lines</span>
</div>

Inside a control, the control does the talking:

import { Button } from "@asheeui/web";

<Button isLoading>Saving invoice</Button>

Inside a control

A spinner in a button is named by the button. There is nothing for the ring to announce: the reader pressed a control, so the control is what has to report that it is working, and the ring is the last few pixels of that report. Button's isLoading does it, the label stays in the accessible name, aria-busy states that it is busy, and a second press is refused while the first is in flight.

The consequence is a rule: never put a spinner in a control that has no other label. An icon-only button still needs its aria-label, and a spinner beside a line of text needs the text to be the thing that is announced, the ring adds nothing to either.

Sizes and colour

size follows the density scale the fields and buttons use, so a ring and the label beside it agree. color is a role rather than a hex value, and it should be the role of the work it reports about: danger inside a destructive action, success while a sync finishes, the application's accent otherwise. speed is a CSS duration, so a genuinely slow import can turn more slowly than the rest of the page.

The rotation is applied under motion-safe, so a reader who asked their system for reduced motion gets a still ring rather than a spinning one, the movement is the embellishment and the label is the report, which is why the ring is never the only thing saying that something is happening.

Props

size is registered as md and speed as 0.75s. The speed is the framework's own shared value, so a configuration file that states it changes the ring on both platforms rather than on the web alone. color is deliberately not registered: it resolves from the application's defaultColor, so a product's accent reaches every spinner without being restated. className is appended last, which is where a consumer's own colour or margin wins. The component forwards the rest of its props, and its ref, to the <svg>.

Accessibility

  • The element is aria-hidden="true", so it has no role, no name and no announcement. Whatever the wait means has to be said by something else on the page.
  • Inside a control, that something is the control: Button with isLoading keeps the action's label in the accessible name, prefixed by a visually hidden Loading, and sets aria-busy so the state is announced rather than only drawn.
  • On its own, wrap it in role="status" and give the wrapper words, a visually hidden line beside the ring, or aria-label, because a region with nothing in it announces nothing.
  • A spinner reserves only its own footprint. Size the region it stands in close to the content that will replace it, so the page does not jump when the wait ends.

The two channels, and why a spinner is deliberately silent while a loading region is not, are described in Announcements.