Status

Components that communicate system feedback and async state.

Loading states

Two complementary patterns for async and in-progress feedback. Use spinnerfor determinate operations where the control point is known (a button, a small region). Use skeleton for content areas that are loading — it preserves layout and reduces perceived latency.

Full reference — Loading states

Spinner

A rotating ring that consumes --spinner-color (defaults to --color-accent) and --spinner-track (20% tint of accent). Apply .spinner alone for the default 24 px size; add .spinner-sm or .spinner-lg for 16 px or 40 px.

ClassSizeBorder
.spinner .spinner-sm16px2px
.spinner24px2px
.spinner .spinner-lg40px3px

Accessibility — spinner

A standalone spinner needs role="status" and aria-label="Loading" so screen readers announce it. When inside a button, the button label already describes the action — use aria-hidden="true" on the spinner element instead to avoid double-announcing.

<!-- Standalone spinner -->
<span class="spinner" role="status" aria-label="Loading"></span>

<!-- Spinner inside a button (button label carries the announcement) -->
<button class="btn btn-p btn-loading" aria-label="Saving">
  <span class="spinner spinner-sm" aria-hidden="true"></span>
  Saving…
</button>

Skeleton

Apply .skeleton to a <span> or <div> and set explicit width and height to match the content it stands in for. Add .skeleton-circle for avatar and icon placeholders.

<!-- Text line placeholders -->
<span class="skeleton" style="display:block;width:60%;height:14px" aria-hidden="true"></span>
<span class="skeleton" style="display:block;width:90%;height:12px" aria-hidden="true"></span>

<!-- Avatar placeholder -->
<span class="skeleton skeleton-circle" style="width:40px;height:40px" aria-hidden="true"></span>

<!-- Image card placeholder -->
<span class="skeleton" style="display:block;width:100%;height:180px;border-radius:var(--radius-lg)" aria-hidden="true"></span>

Accessibility — skeleton

Mark all skeleton elements aria-hidden="true" — they are visual scaffolding, not content. Announce the loading state to screen readers with a separate live region:

<div aria-live="polite" aria-busy="true" class="sr-only">Loading content…</div>
<!-- skeleton markup here -->

Remove or update the live region once content loads.

Reduced motion

Both components respect prefers-reduced-motion: reduce. The spinner stops rotating (ring remains visible as a static indicator). The skeleton disables the shimmer and renders as a static tinted block — both still communicate "something is here but not yet loaded."

Token reference

TokenDefaultPurpose
--spinner-colorvar(--color-accent)Active arc colour
--spinner-track20% tint of accentBackground ring colour
--spinner-size24pxDefault spinner diameter
--spinner-sm-size16pxSmall spinner diameter
--spinner-lg-size40pxLarge spinner diameter
--spinner-border2pxDefault + sm ring width
--spinner-lg-border3pxLarge ring width
--spinner-durationvar(--duration-loop)Rotation cycle duration
--skeleton-radiusvar(--radius-sm)Placeholder corner radius
--skeleton-basevar(--color-bg-panel)Placeholder base colour
--skeleton-shine--bm2-birch light / --in2-slate darkShimmer highlight colour — per-theme palette value
--skeleton-durationvar(--duration-loop)Shimmer cycle duration

CSS reference

SelectorPurpose
.spinnerDefault 24px rotating ring
.spinner-sm16px size modifier (add to .spinner)
.spinner-lg40px size modifier (add to .spinner)
.skeletonShimmer placeholder rectangle
.skeleton-circleMakes skeleton circular (add to .skeleton)