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.
| Class | Size | Border |
|---|---|---|
.spinner .spinner-sm | 16px | 2px |
.spinner | 24px | 2px |
.spinner .spinner-lg | 40px | 3px |
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
| Token | Default | Purpose |
|---|---|---|
--spinner-color | var(--color-accent) | Active arc colour |
--spinner-track | 20% tint of accent | Background ring colour |
--spinner-size | 24px | Default spinner diameter |
--spinner-sm-size | 16px | Small spinner diameter |
--spinner-lg-size | 40px | Large spinner diameter |
--spinner-border | 2px | Default + sm ring width |
--spinner-lg-border | 3px | Large ring width |
--spinner-duration | var(--duration-loop) | Rotation cycle duration |
--skeleton-radius | var(--radius-sm) | Placeholder corner radius |
--skeleton-base | var(--color-bg-panel) | Placeholder base colour |
--skeleton-shine | --bm2-birch light / --in2-slate dark | Shimmer highlight colour — per-theme palette value |
--skeleton-duration | var(--duration-loop) | Shimmer cycle duration |
CSS reference
| Selector | Purpose |
|---|---|
.spinner | Default 24px rotating ring |
.spinner-sm | 16px size modifier (add to .spinner) |
.spinner-lg | 40px size modifier (add to .spinner) |
.skeleton | Shimmer placeholder rectangle |
.skeleton-circle | Makes skeleton circular (add to .skeleton) |