Spinner

Indeterminate progress indicator. Two motion vocabularies ship: a rotating ring (default) painted with one solid arc on a low-alpha track, and a pulsing disc (.is-grow) for placements where a rotating ring would compete with surrounding text. Three sizes (small / default / large) and the shared .is-* combinations carry through.

Live

Rotating ring at three sizes

<div style="display: flex; gap: 1rem; align-items: center;">
<span class="spinner is-small" role="status" aria-label="Loading"></span>
<span class="spinner" role="status" aria-label="Loading"></span>
<span class="spinner is-large" role="status" aria-label="Loading"></span>
</div>
@use '@adnap/krysalicss' with (
  $feature-list: ('base-reset', 'base-global', 'element-spinner')
);
Playground

Pulsing disc (.is-grow)

<div style="display: flex; gap: 1rem; align-items: center;">
<span class="spinner is-grow is-small" role="status" aria-label="Loading"></span>
<span class="spinner is-grow" role="status" aria-label="Loading"></span>
<span class="spinner is-grow is-large" role="status" aria-label="Loading"></span>
</div>
@use '@adnap/krysalicss' with (
  $feature-list: ('base-reset', 'base-global', 'element-spinner')
);
Playground

Combinations (.is-primary / .is-warning / .is-danger / .is-success)

<div style="display: flex; gap: 1rem; align-items: center; flex-wrap: wrap;">
<span class="spinner is-primary" role="status" aria-label="Loading"></span>
<span class="spinner is-warning" role="status" aria-label="Loading"></span>
<span class="spinner is-danger" role="status" aria-label="Loading"></span>
<span class="spinner is-success" role="status" aria-label="Loading"></span>
<span class="spinner is-grow is-primary" role="status" aria-label="Loading"></span>
<span class="spinner is-grow is-warning" role="status" aria-label="Loading"></span>
<span class="spinner is-grow is-danger" role="status" aria-label="Loading"></span>
<span class="spinner is-grow is-success" role="status" aria-label="Loading"></span>
</div>
@use '@adnap/krysalicss' with (
  $feature-list: ('base-reset', 'base-global', 'element-spinner')
);
Playground

Markup

markup
<!-- Default rotating ring. -->
<span class="spinner" role="status" aria-label="Loading"></span>

<!-- Pulsing disc variant. -->
<span class="spinner is-grow" role="status" aria-label="Loading"></span>

<!-- Brand-tinted, large. -->
<span class="spinner is-primary is-large" role="status" aria-label="Loading"></span>

Modes

  • Rotating ring (default). A circular border with a low-alpha track and one solid arc on the block-start edge, rotated continuously. Reads as the canonical browser "loading" affordance.
  • .is-grow. Swaps the ring for a filled disc that scales from 0 to 1 while fading out. Same accessibility semantics; useful when the spinner overlaps text or sits inside a tight slot where ring strokes feel noisy.

Sizes

  • .is-small1rem square.
  • (default) — 1.5rem square.
  • .is-large2.5rem square.

The border thickness is expressed in em so the ring scales proportionally with the surrounding font-size when the spinner is dropped inside a button or heading.

Combinations

The four .is-* combinations (is-primary, is-warning, is-danger, is-success) tint the arc to the combination's background colour — the arc IS the visible surface here, so the brand-signature surface bg carries the right semantic (same rationale as Progress). The default spinner paints in the default combination foreground.

Accessibility

  • Always pair with role="status" and an aria-label ("Loading…", "Saving…", etc.) so screen readers announce the state. The visual is purely decorative; the announcement does the actual conveying.
  • Under prefers-reduced-motion: reduce the rotation slows to 2s ($reduced-motion-duration) and the .is-grow pulse is replaced with a static half-opacity disc. A frozen spinner reads as "broken"; a slow rotation still signals "in progress" without triggering vestibular discomfort (WCAG 2.3.3).
  • Under forced-colors: active the color-mix track collapses; the plugin pins the track to CanvasText and the arc to Highlight so the disc shape stays defined (WCAG 1.4.11).

Variables

VariableDefaultNotes
$selector'.spinner'Re-scope freely.
$grow-selector'.is-grow'Modifier swapping ring for pulsing disc.
$small-selector'.is-small'Small-size modifier.
$large-selector'.is-large'Large-size modifier.
$size1.5remDefault square size.
$small-size1remSmall-size square.
$large-size2.5remLarge-size square.
$border-width0.25emRing thickness. em-relative so it tracks the parent font-size.
$duration0.75sRotation cycle. Linear (not eased) so the motion feels mechanical.
$grow-duration1sPulse cycle for .is-grow.
$track-mix25%Static-track tint (currentColor over transparent). Mixing against transparent (not bg) keeps the ring overlay-safe on coloured surfaces.
$default-combination$color-combination-defaultFallback tuple when no --kc-spinner-fg is supplied.
$reduced-motion-duration2sSlowed rotation under prefers-reduced-motion: reduce.

Override example

app.scss
@use '@adnap/krysalicss/element/spinner' with (
  $size: 2rem,
  $border-width: 0.2em,
  $duration: 1s,
  $track-mix: 18%,
);

Tokens consumed

TokenUsed for
--kc-spinner-fgDefault arc colour (and disc fill under .is-grow).
--kc-{label}-bgPer-combination arc colour. The arc paints in the combination's bg — the visible surface carries the brand-signature colour.