Loading & spinners

ui.css · Cross-system: Cross-system reference § Content

Overview

There is one shared loading spinner: the .spin utility on a <ui-icon>. Use it to keep loading indicators consistent. Different types of loading indication may legitimately look different (a full-view busy overlay is not an inline glyph), but each type has one canonical implementation. Pick from the table below.

Choosing a loading indicator

SituationUseNotes
Button during submission; inline action <ui-icon name="spinner" class="spin"> Disable the control and swap its label — see Inline spinner.
Form field awaiting the server's verdict on its own value with_load_feedback(group, promise) → the .input-group updating tier Target the group: the muted border and cursor: progress are an .input-group rule, so aria-busy on a .btn is an ARIA-only state that has no visual effect — a button awaiting submission still needs the indicator described in the row above. Debounced 200 ms, so a response received before the timer expires shows no busy state. For the complete sequence from initial hint to server response, see Asking the server.
Status callout / row <ui-icon name="spinner" class="spin"> + verb text e.g. <ui-icon…></ui-icon> Saving…
Whole view or dialog loading <ui-outlet> / <ui-dialog> debounced aria-busy The framework renders a centered ring + dims prior content automatically. Don't add an inline spinner in a view body. See Overlay.
Determinate progress (%, steps) native <progress value max> Implicit role="progressbar"; add aria-label.
Indeterminate long task (bar, not glyph) native <progress> (omit value) Omitting value makes it indeterminate.

.spin — the inline spinner

Continuous rotation for inline loading. Add class="spin" to a <ui-icon name="spinner">. The icon inherits its size from the surrounding font-size and its color from currentColor; the rotation speed comes from the --animation-spin token.

Page / component loading

Active on load, fades out when done. Set .spin directly in markup.

Show code

On-demand — the spin() helper

Initially hidden, toggled by spin(icon, active) from lib/ui.mjs. The helper fades the icon in/out, toggles the .spin class and hidden, and adds role="status" while active (removed when done) so screen readers announce it once and then stop. Give the icon an aria-label in markup for the announced text.

Show code

Button during submission

Disable the button and swap its label; restore in finally.

btn.disabled = true btn.innerHTML = '<ui-icon name="spinner" class="spin" aria-hidden="true"></ui-icon> Saving…' try { await api.save(…) } finally { btn.disabled = false btn.innerHTML = 'Save' }

View / dialog overlay

<ui-outlet> and <ui-dialog> toggle a debounced aria-busy="true" while a view loads: an async factory or promise passed to show(), then the view's .readyP promise (attribute load-feedback-delay, default 200 ms — fast loads never flash). The default cue, which each component injects ahead of the page's stylesheets, is a centered ring that dims prior content. This is a deliberately distinct type of indicator (a big overlay, not an inline glyph); apps can override [aria-busy="true"] to substitute a skeleton or a different spinner. See <ui-outlet> and <ui-dialog>, which debounce aria-busy while their content loads. Use an inline spinner for other contexts: a button mid-submit, a status row, a callout.

For a field awaiting a server response, use with_load_feedback(group, promise) on its .input-group. This applies the updating state after the same debounce as the view overlay, blocks pointer input, and sets the busy state for assistive technology. An optional spinner in .suffix supplements that state. See Form field states § Asking the server.

Accessibility

See also