Photon Design System

Spinner

An indeterminate loading ring — a hairline track with a quarter arc rotating over it, in the same continuous round-cap stroke as Photon’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 (default md)
  • aria-label — string — when set, the spinner is announced (and no longer aria-hidden)
  • className — string — size overrides, ink (text-pho-*), layout
  • …also accepts native <svg> attributes

Accessibility

  • Decorative by default — aria-hidden is 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, async Menu.Item / Avatar.Edit) so focus and naming stay on the action.

Best practices

  • Use Spinner for indeterminate wait; use Progress when you know percent complete.
  • Keep size close to neighboring icons (md next to 16px glyphs).
  • Don’t stack a naked spinner and a “Loading…” label that both announce — hide one from assistive tech.