Spinner
An indeterminate loading ring — a hairline track with a quarter arc rotating
over it, in the same continuous round-cap stroke as Pho’s hairlines and
icons. Uses currentColor, so ink follows the parent — pair with
text-pho-* or place it on a light/dark surface. Decorative by default; pass
aria-label when the spinner is the only loading cue. Prefer Button loading / Menu.Item async / Avatar.Edit loading when the spinner belongs
to a control.
Import
import { Spinner } from "@photon-ai/pho-ui/components/spinner";
Sizes
Three sizes — sm, md, lg. The default is md (matches menu rows and
button icon slots).
sm
md
lg
<Spinner size="sm" />
<Spinner size="md" />
<Spinner size="lg" />
Ink
Tint with a text utility. On overlays, inherit white from the parent.
<Spinner className="text-pho-brand" aria-label="Loading" />
With Button
Controls that already expose loading use this same ring — you rarely need to
compose Spinner yourself on a button.
<Button loading>Saving</Button>
Props
- size —
sm·md·lg(defaultmd) - aria-label —
string— when set, the spinner is announced (and no longeraria-hidden) - className —
string— size overrides, ink (text-pho-*), layout - …also accepts native
<svg>attributes
Accessibility
- Decorative by default —
aria-hiddenis set so screen readers skip the glyph when a parent already names the busy state (aria-busy, button label). - Standalone — if the spinner is the only cue, pass
aria-label(e.g."Loading"). - Prefer wiring loading on the host control (
Button loading, asyncMenu.Item/Avatar.Edit) so focus and naming stay on the action.
Best practices
- Use
Spinnerfor indeterminate wait; useProgresswhen you know percent complete. - Keep size close to neighboring icons (
mdnext to 16px glyphs). - Don’t stack a naked spinner and a “Loading…” label that both announce — hide one from assistive tech.