ui.css · Cross-system: Cross-system reference § Content
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.
| Situation | Use | Notes |
|---|---|---|
| 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.
Active on load, fades out when done. Set .spin directly in markup.
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.
Disable the button and swap its label; restore in finally.
<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.
role="status" with an aria-label; clear the label / role when loading completes so screen readers stop announcing "Loading". The spin() helper manages role for you.aria-hidden="true" — the text is the status, so the icon would only double-announce.prefers-reduced-motion rather than disabled — unlike decorative animations. See Motion.--animation-spin, restore a calmer rate under @media (prefers-reduced-motion: reduce). Changing the shared token keeps all spinners consistent.
<ui-icon> — the icon component and searchable glyph grid.--animation-spin token and reduce-motion pattern..input-group — the group that carries a field's busy tier, and the .suffix slot a spinner goes in.with_load_feedback demo.