@ -9,9 +9,16 @@
import { Stack } from '$uix/eidos/components/stack';
import { Stack } from '$uix/eidos/components/stack';
import { compileMorfo } from '$uix/morfo';
import { compileMorfo } from '$uix/morfo';
import { spinnerMorfo } from '@/uix/morfo/components/spinner';
import { spinnerMorfo } from '@/uix/morfo/components/spinner';
import { getActiveUix } from '$active-uix';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type TraceEntry = { event : string ; family : string ; intent? : string ; at : number } ;
const uix = getActiveUix();
let tab = $state< Tab > ('live');
let tab = $state< Tab > ('live');
let trace = $state< TraceEntry [ ] > ([]);
let stageRef = $state< HTMLElement | null > (null);
let variant = $state< SpinnerVariant > ('ring');
let variant = $state< SpinnerVariant > ('ring');
let size = $state< SpinnerSize > ('md');
let size = $state< SpinnerSize > ('md');
@ -35,19 +42,71 @@
'loss'
'loss'
];
];
$effect(() => {
const el = stageRef;
if (!el) return;
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.attributeName !== 'data-event') continue;
const target = m.target as Element;
const ev = target.getAttribute('data-event');
if (!ev) continue;
trace = [
{
event: ev,
family: target.getAttribute('data-event-family') ?? '—',
intent: target.getAttribute('data-event-intent') ?? undefined,
at: Date.now()
},
...trace
].slice(0, 6);
}
});
obs.observe(el, { attributes : true , subtree : true , attributeFilter : [ 'data-event' ] } );
return () => obs.disconnect();
});
const somaSnippet = $derived(
[
'<!-- Spinner is eidos - native — no soma layer. --> ',
'<!-- Equivalent semantic markup (not real soma): --> ',
'',
'< div ' ,
' data-spinner',
variant !== 'ring' && ` data-variant="${ variant } "`,
size !== 'md' && ` data-size="${ size } "`,
color !== 'primary' && ` data-color="${ color } "`,
' role="status"',
' aria-live="polite"',
' aria-label="Loading"',
'>',
variant === 'ring' & & ' < span data-spinner-track aria-hidden = "true" > < / span > ',
variant === 'dots' & &
' < span data-spinner-track aria-hidden = "true" > ' +
'< span data-spinner-dot > < / span > < span data-spinner-dot > < / span > < span data-spinner-dot > < / span > < / span > ',
variant === 'bars' & &
' < span data-spinner-track aria-hidden = "true" > ' +
'< span data-spinner-bar > < / span > < span data-spinner-bar > < / span > < span data-spinner-bar > < / span > < span data-spinner-bar > < / span > < / span > ',
withLabel & & ' < span data-spinner-label > Loading…< / span > ',
'< / div > '
]
.filter(Boolean)
.join('\n')
);
const eidosSnippet = $derived(
const eidosSnippet = $derived(
[
[
"< script lang = 'ts' > " ,
"< script lang = 'ts' > " ,
" import { Spinner } from '$uix/eidos/components/spinner';",
" import { Spinner } from '$uix/eidos/components/spinner';",
'< /' + 'script>',
'< /' + 'script>',
'',
'',
'< Spinner ' ,
withLabel ? '< Spinner' : ' < Spinner' ,
variant !== 'ring' && ` variant="${ variant } "`,
variant !== 'ring' && ` variant="${ variant } "`,
size !== 'md' && ` size="${ size } "`,
size !== 'md' && ` size="${ size } "`,
color !== 'primary' && ` color="${ color } "`,
color !== 'primary' && ` color="${ color } "`,
'>',
withLabel ? '>' : '/ >',
withLabel & & ' Loading…',
withLabel & & ' Loading…',
'< / Spinner > '
withLabel & & '< / Spinner > '
]
]
.filter(Boolean)
.filter(Boolean)
.join('\n')
.join('\n')
@ -59,23 +118,37 @@
< div data-uix-eyebrow > Feedback · Spinner< / div >
< div data-uix-eyebrow > Feedback · Spinner< / div >
< h1 data-uix-page-title > Spinner< / h1 >
< h1 data-uix-page-title > Spinner< / h1 >
< p data-uix-page-lede >
< p data-uix-page-lede >
Indeterminate progress indicator. Three visual variants (< code > ring< / code > ,
Indeterminate progress indicator. Three visual variants
< code > dots< / code > , < code > bars< / code > ), five sizes, eight color accents, and an optional
(< code > ring< / code > , < code > dots< / code > , < code > bars< / code > ), five sizes, eight color accents,
inline label. Pure eidos primitive — no semantic events. Use
and an optional inline label. Eidos-native: no semantic events, no soma split — the morfo
< code > < Progress> < / code > instead when you have a known percentage.
declares two parts (Provider + decorative Track). Use
< a href = "/uix/components/progress" > < Progress> < / a > instead when you have a known
percentage.
< / p >
< / p >
< div data-uix-page-meta >
< div data-uix-page-meta >
< span data-uix-meta-pill >< span data-uix-meta-key > parts</ span > { partsList . length } </ span >
< span data-uix-meta-pill >
< span data-uix-meta-pill > < span data-uix-meta-key > events< / span > 0< / span >
< span data-uix-meta-key > parts</ span > { compiled . parts . order . length }
< span data-uix-meta-pill >< span data-uix-meta-key > variants</ span > { variants . length } </ span >
< / span >
< span data-uix-meta-pill > < span data-uix-meta-key > scope< / span > eidos< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > events< / span > 0
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > variants</ span > { variants . length }
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > sizes</ span > { sizes . length }
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > scope< / span > eidos
< / span >
< / div >
< / div >
< / header >
< / header >
<!-- Live preview always rendered (between header and tabs). -->
< div data-uix-stage >
< div data-uix-stage >
< div data-uix-stage-area >
< div data-uix-stage-area bind:this = {stageRef } >
< Box
< Box
style="padding: var(--uix-space-6); display: flex; align-items: center; justify-content: center;"
style="padding: var(--uix-space-6); display: flex; align-items: center; justify-content: center; inline-size: 100%; "
>
>
{ #if withLabel }
{ #if withLabel }
< Spinner { variant } { size } { color } > Loading…</ Spinner >
< Spinner { variant } { size } { color } > Loading…</ Spinner >
@ -84,20 +157,31 @@
{ /if }
{ /if }
< / Box >
< / Box >
< / div >
< / div >
< div data-uix-stage-trace >
< span data-uix-stage-trace-key > trace< / span >
< span > { trace . length === 0 ? 'no semantic events' : `$ { trace . length } event ( s ) ` } </ span >
< span style = "color: var(--uix-text-faint)" > ·< / span >
< span data-uix-stage-trace-key > variant< / span >
< span > { variant } </ span >
< span style = "margin-inline-start: auto;" >
< span data-uix-stage-trace-key > size< / span >
{ size } · < span data-uix-stage-trace-key > color</ span > { color }
< / span >
< / div >
< / div >
< / div >
< div data-uix-tabs role = "tablist" >
< div data-uix-tabs role = "tablist" >
< button data-uix-tab data-active = { tab === 'live' } onclick= {() => ( tab = 'live' )} > Live</ button >
< button data-uix-tab data-active = { tab === 'live' } onclick= {() => ( tab = 'live' )} > Live</ button >
< button data-uix-tab data-active = { tab === 'api' } onclick= {() => ( tab = 'api' )}
< button data-uix-tab data-active = { tab === 'api' } onclick= {() => ( tab = 'api' )} >
>API < span data-uix-tab-count > 5< / span > < /button
API < span data-uix-tab-count > 5< / span >
>
< / button >
< button data-uix-tab data-active = { tab === 'morfo' } onclick= {() => ( tab = 'morfo' )} >
< button data-uix-tab data-active = { tab === 'morfo' } onclick= {() => ( tab = 'morfo' )} >
< span data-uix-layer-badge = "morfo" > morfo< / span >
< span data-uix-layer-badge = "morfo" > morfo< / span >
< span data-uix-tab-count > { partsList . length } p · 0e</ span >
< span data-uix-tab-count > { partsList . length } p · 0e</ span >
< / button >
< / button >
< button data-uix-tab data-active = { tab === 'sema' } onclick= {() => ( tab = 'sema' )} >
< button data-uix-tab data-active = { tab === 'sema' } onclick= {() => ( tab = 'sema' )} >
< span data-uix-layer-badge = "sema" > sema< / span >
< span data-uix-layer-badge = "sema" > sema< / span >
< span data-uix-tab-count > 0< /span >
< span data-uix-tab-count >{ events . length } < /span >
< / button >
< / button >
< button data-uix-tab data-active = { tab === 'recipe' } onclick= {() => ( tab = 'recipe' )} > Recipe< /button
< button data-uix-tab data-active = { tab === 'recipe' } onclick= {() => ( tab = 'recipe' )} > Recipe< /button
>
>
@ -107,9 +191,15 @@
{ #if tab === 'live' }
{ #if tab === 'live' }
< section data-uix-section >
< section data-uix-section >
< h2 data-uix-section-title > Controls< / h2 >
< h2 data-uix-section-title > Controls< / h2 >
< p data-uix-section-desc >
Spinner is eidos-native — no < span data-uix-layer-badge = "soma" > soma< / span > split. The
< code > variant< / code > prop swaps the inner Track DOM (one arc vs three dots vs four bars);
< code > size< / code > drives a single < code > --_spinner-size< / code > token that all three
geometries share.
< / p >
< div data-uix-subsection-head >
< div data-uix-subsection-head >
< span data-uix-layer-badge = "eidos" > eidos< / span > props
< span data-uix-layer-badge = "eidos" > eidos< / span > props · visual treatment
< / div >
< / div >
< div data-uix-controls >
< div data-uix-controls >
< label data-uix-control >
< label data-uix-control >
@ -148,114 +238,342 @@
< / label >
< / label >
< / div >
< / div >
< div data-uix-subsection-head > All variants side-by-side< / div >
<!-- ── Code snippets per layer ─────────────────────────────────── -->
< Stack gap = "var(--uix-space-4)" >
< div data-uix-code >
{ #each variants as v ( v )}
< div data-uix-code-head >
< Box
< span data-uix-layer-badge = "soma" > soma< / span >
style="display: flex; gap: var(--uix-space-6); align-items: center; padding: var(--uix-space-3); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md);"
< span > n/a · spinner is eidos-native — equivalent markup shown< / span >
>
< span data-uix-code-lang > svelte< / span >
< code style = "min-inline-size: 4rem;" > { v } </ code >
< / div >
{ #each sizes as s ( s )}
< pre >< code > { somaSnippet } </ code ></ pre >
< Spinner variant = { v } size= { s } { color } />
< / div >
{ /each }
< / Box >
{ /each }
< / Stack >
< div data-uix-subsection-head > Color matrix< / div >
< div data-uix-code style = "margin-top: var(--uix-space-3);" >
< Box style = "display: flex; gap: var(--uix-space-5); flex-wrap: wrap; padding: var(--uix-space-3);" >
< div data-uix-code-head >
{ #each colors as c ( c )}
< span data-uix-layer-badge = "eidos" > eidos< / span >
< Box style = "display: flex; flex-direction: column; align-items: center; gap: var(--uix-space-2);" >
< span > visual · variant swaps Track DOM< / span >
< Spinner { variant } { size } color = { c } / >
< span data-uix-code-lang > svelte< / span >
< code > { c } </ code >
< / div >
< / Box >
< pre >< code > { eidosSnippet } </ code ></ pre >
{ /each }
< / div >
< / Box >
< / section >
{ :else if tab === 'api' }
< section data-uix-section >
< h2 data-uix-section-title > API< / h2 >
< table data-uix-api-table >
< thead >
< tr > < th > prop< / th > < th > type< / th > < th > default< / th > < th > description< / th > < / tr >
< / thead >
< tbody >
< tr > < td > variant< / td > < td > 'ring' | 'dots' | 'bars'< / td > < td > 'ring'< / td > < td > Visual style.< / td > < / tr >
< tr > < td > size< / td > < td > 'xs'..'xl'< / td > < td > 'md'< / td > < td > Track diameter / dot size.< / td > < / tr >
< tr > < td > color< / td > < td > ColorRole< / td > < td > 'primary'< / td > < td > Accent palette.< / td > < / tr >
< tr > < td > aria-label< / td > < td > string?< / td > < td > —< / td > < td > Override the translated 'Loading'.< / td > < / tr >
< tr > < td > children< / td > < td > Snippet?< / td > < td > —< / td > < td > Optional inline label.< / td > < / tr >
< / tbody >
< / table >
< / section >
< / section >
{ :else if tab === 'morfo' }
{ /if }
{ #if tab === 'api' }
< section data-uix-section >
< section data-uix-section >
< h2 data-uix-section-title >
< h2 data-uix-section-title > API reference< / h2 >
< span data-uix-layer-badge = "morfo" > morfo< / span > contract
< / h2 >
< p data-uix-section-desc >
< p data-uix-section-desc >
Two parts: < code > provider< / code > (role=status, polite live region) and < code > track< / code >
Spinner exposes variant / size / color and an optional inline label snippet. Inherits
(decorative, aria-hidden). The variant prop only swaps the inner DOM of Track .
the full Box prop surface (gap, gridColumn, …) so it composes inline next to text.
< / p >
< / p >
< table data-uix-api-table >
< thead > < tr > < th > part< / th > < th > element< / th > < th > role< / th > < th > archetype< / th > < / tr > < / thead >
< div data-uix-subsection-head > Spinner-specific props< / div >
< tbody >
< div data-uix-table-wrap >
{ #each partsList as p ( p . kebab )}
< table data-uix-table >
< thead > < tr > < th > Prop< / th > < th > Type< / th > < th > Notes< / th > < / tr > < / thead >
< tbody >
< tr >
< td class = "name" > variant< / td >
< td class = "type" > 'ring' | 'dots' | 'bars'< / td >
< td >
< code > ring< / code > (default) — rotating arc, lightest. < code > dots< / code > —
three bouncing dots, medium weight. < code > bars< / code > — four stretching bars,
most attention.
< / td >
< / tr >
< tr >
< td class = "name" > size< / td >
< td class = "type" > 'xs' | 'sm' | 'md' | 'lg' | 'xl'< / td >
< td > Track diameter / dot size / bar height. Default < code > md< / code > .< / td >
< / tr >
< tr >
< td class = "name" > color< / td >
< td class = "type" > ColorRole< / td >
< td > Accent palette. Default < code > primary< / code > .< / td >
< / tr >
< tr >
< td class = "name" > aria-label< / td >
< td class = "type" > string?< / td >
< td > Override the translated 'Loading' default — use to give context.< / td >
< / tr >
< tr >
< td class = "name" > children< / td >
< td class = "type" > Snippet?< / td >
< td > Optional inline label rendered next to the spinner.< / td >
< / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Reference comparison< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Library< / th > < th > Closest equivalent< / th > < th > Difference< / th > < / tr > < / thead >
< tbody >
< tr >
< td class = "name" > chakra-ui< / td >
< td > < code > < Spinner> < / code > < / td >
< td > Chakra has one variant (ring). UIX adds < code > dots< / code > + < code > bars< / code > for higher-weight contexts.< / td >
< / tr >
< tr >
< td class = "name" > mantine< / td >
< td > < code > < Loader> < / code > < / td >
< td > Mantine's Loader exposes three variants by the same names. UIX keeps the API but uses < code > role="status"< / code > over Mantine's plain div.< / td >
< / tr >
< tr >
< td class = "name" > radix-themes< / td >
< td > < code > < Spinner> < / code > < / td >
< td > Radix only ships the ring. UIX matches the API surface, adds variants + the inline-label snippet.< / td >
< / tr >
< tr >
< td class = "name" > react-aria< / td >
< td > < code > < ProgressBar isIndeterminate> < / code > < / td >
< td > React Aria reuses ProgressBar in indeterminate mode. UIX separates concerns — < code > < Progress> < / code > for determinate, < code > < Spinner> < / code > for indeterminate.< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / section >
{ /if }
{ #if tab === 'morfo' }
< section data-uix-section >
< h2 data-uix-section-title > Morfo contract< / h2 >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Field< / th > < th > Value< / th > < / tr > < / thead >
< tbody >
< tr >< td class = "name" > name</ td >< td > { spinnerMorfo . name } </ td ></ tr >
< tr >< td class = "name" > kebab</ td >< td >< code > { spinnerMorfo . kebab } </ code ></ td ></ tr >
< tr >< td class = "name" > scope</ td >< td > { spinnerMorfo . scope . join ( ', ' )} </ td ></ tr >
< tr >< td class = "name" > parts</ td >< td > { partsList . length } </ td ></ tr >
< tr > < td class = "name" > events< / td > < td > 0< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Parts< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead >
< tr >
< th > kebab< / th >
< th > marker< / th >
< th > element< / th >
< th > archetype< / th >
< th > role< / th >
< th > optional< / th >
< / tr >
< / thead >
< tbody >
{ #each partsList as part ( part . kebab )}
< tr >
< td class = "name" > { part . kebab } </ td >
< td >< code data-uix-part-marker > [{ part . marker } ]</ code ></ td >
< td class = "type" > < { part . defaultElement } > </ td >
< td class = "type" > { part . archetype ?? '—' } </ td >
< td class = "type" > { part . role ?? '—' } </ td >
< td class = "default" > { part . optional ? 'yes' : 'no' } </ td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Provider · aria< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > attr< / th > < th > value< / th > < th > severity< / th > < / tr > < / thead >
< tbody >
< tr >
< td class = "name" > < code > role< / code > < / td >
< td > status< / td >
< td class = "type" > part default< / td >
< / tr >
< tr >
< td class = "name" > < code > aria-label< / code > < / td >
< td > translationRef('components.spinner.label')< / td >
< td class = "type" > recommended< / td >
< / tr >
< tr >
< td class = "name" > < code > aria-live< / code > < / td >
< td > "polite"< / td >
< td class = "type" > literal< / td >
< / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Track · aria< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > attr< / th > < th > value< / th > < th > severity< / th > < / tr > < / thead >
< tbody >
< tr >
< tr >
< td >< code > { p . kebab } </ code ></ td >
< td class = "name" > < code > aria-hidden< / code > < / td >
< td >< code > { p . defaultElement } </ code ></ td >
< td > "true"< / td >
< td > { p . role ?? '—' } </ td >
< td class = "type" > literal< / td >
< td > { p . archetype ?? '—' } </ td >
< / tr >
< / tr >
{ /each }
</ tbody >
< / tbody >
< / t able >
< / table >
< / div >
< / section >
< / section >
{ :else if tab === 'sema' }
{ /if }
{ #if tab === 'sema' }
< section data-uix-section >
< section data-uix-section >
< h2 data-uix-section-title >
< h2 data-uix-section-title >
< span data-uix-layer-badge = "sema" > sema< / span > events
< span data-uix-layer-badge = "sema" > sema< / span > · events
< / h2 >
< / h2 >
< p data-uix-section-desc >
< p data-uix-section-desc >
Spinner has no semantic events — the loading state is announced only through ARIA. There's
Spinner declares < strong > no semantic events< / strong > . The loading state announces only
no commit/emerge/shift to react to. Mount when work starts; unmount when it ends.
through ARIA — there is no commit/emerge/shift to react to. Mount the spinner when work
starts; unmount when it ends.
< / p >
< p data-uix-section-desc >
If you need a perceptual cue when the work starts or finishes, attach it to the parent
action (the form submission that triggered the fetch, the route navigation that opened
the page) — not to Spinner.
< / p >
< / p >
< / section >
< / section >
{ :else if tab === 'recipe' }
{ /if }
{ #if tab === 'recipe' }
< section data-uix-section >
< section data-uix-section >
< h2 data-uix-section-title > Recipe< / h2 >
< h2 data-uix-section-title > Eidos recipe< / h2 >
< div data-uix-subsection-head >
< p data-uix-section-desc >
< span data-uix-layer-badge = "eidos" > eidos< / span > · canonical usage
Recipe lives in < code > src/uix/eidos/components/spinner/spinner.css< / code > . All three
variants share private tokens (< code > --_spinner-size< / code > ,
< code > --_spinner-color< / code > , < code > --_spinner-track< / code > ); each variant adds its own
keyframes block.
< / p >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Selector< / th > < th > Owner< / th > < th > Purpose< / th > < / tr > < / thead >
< tbody >
< tr >
< td class = "name" > < code > [data-spinner]< / code > < / td >
< td > < span data-uix-tag data-kind = "morfo" > morfo< / span > < / td >
< td > Provider marker. Inline-flex container with label gap.< / td >
< / tr >
< tr >
< td class = "name" > < code > [data-spinner][data-size='xs'..'xl']< / code > < / td >
< td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td >
< td > Sets < code > --_spinner-size< / code > + < code > --_spinner-thickness< / code > .< / td >
< / tr >
< tr >
< td class = "name" > < code > [data-spinner][data-color='primary'..'loss']< / code > < / td >
< td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td >
< td > Maps to color-mix accent + track tokens.< / td >
< / tr >
< tr >
< td class = "name" > < code > [data-spinner-track]< / code > < / td >
< td > < span data-uix-tag data-kind = "morfo" > morfo< / span > < / td >
< td > Track part marker (aria-hidden decoration).< / td >
< / tr >
< tr >
< td class = "name" > < code > [data-spinner][data-variant='ring'] [data-spinner-track]< / code > < / td >
< td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td >
< td > Rotating arc via border-top-color contrast + < code > spinner-rotate< / code > keyframes.< / td >
< / tr >
< tr >
< td class = "name" > < code > [data-spinner][data-variant='dots'] [data-spinner-dot]< / code > < / td >
< td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td >
< td > Three dots with staggered < code > spinner-bounce< / code > delays.< / td >
< / tr >
< tr >
< td class = "name" > < code > [data-spinner][data-variant='bars'] [data-spinner-bar]< / code > < / td >
< td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td >
< td > Four bars with staggered < code > spinner-stretch< / code > delays.< / td >
< / tr >
< tr >
< td class = "name" > < code > @media (prefers-reduced-motion: reduce)< / code > < / td >
< td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td >
< td > Freezes all keyframes; ring loses the top-color contrast for low-distraction.< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / div >
< pre data-uix-snippet >< code > { eidosSnippet } </ code ></ pre >
< div data-uix-subsection-head > All variants × sizes< / div >
< Stack gap = "var(--uix-space-4)" >
{ #each variants as v ( v )}
< Box
style="display: flex; gap: var(--uix-space-6); align-items: center; padding: var(--uix-space-3); border: 1px solid var(--uix-border); border-radius: var(--uix-radius-md);"
>
< code style = "min-inline-size: 4rem;" > { v } </ code >
{ #each sizes as s ( s )}
< Spinner variant = { v } size= { s } { color } />
{ /each }
< / Box >
{ /each }
< / Stack >
< / section >
< / section >
{ :else if tab === 'a11y' }
{ /if }
{ #if tab === 'a11y' }
< section data-uix-section >
< section data-uix-section >
< h2 data-uix-section-title > Accessibility< / h2 >
< h2 data-uix-section-title > Accessibility< / h2 >
< ul >
< div data-uix-table-wrap >
< li >
< table data-uix-table >
< code > role="status"< / code > + < code > aria-live="polite"< / code > on the root — the screen
< thead > < tr > < th > Concern< / th > < th > Contract< / th > < / tr > < / thead >
reader announces the configured label when the spinner enters the DOM (typically when
< tbody >
you start a fetch).
< tr >
< / li >
< td class = "name" > Role< / td >
< li >
< td >
Default label is the translated "Loading" / "Cargando". Override per-instance with
< code > role="status"< / code > on the Provider — a polite live region. Screen
< code > aria-label="Loading orders"< / code > for more specific context.
readers announce the label when the spinner enters the DOM.
< / li >
< / td >
< li >
< / tr >
Visual track parts (< code > data-spinner-track< / code > , < code > -dot< / code > , < code > -bar< / code > )
< tr >
are < code > aria-hidden="true"< / code > — decoration only.
< td class = "name" > Live region< / td >
< / li >
< td >
< li >
< code > aria-live="polite"< / code > — announcements wait for the user to finish
Animation respects < code > prefers-reduced-motion< / code > — under reduced motion the
their current speech utterance before being read.
rotation/bounce/stretch keyframes stop, but the visual stays so the user still sees the
< / td >
loading affordance.
< / tr >
< / li >
< tr >
< li >
< td class = "name" > Label< / td >
Use < code > < Spinner> < / code > for unknown progress (network requests, async
< td >
rendering). Switch to < code > < Progress> < / code > when you can report a percentage.
Defaults to the translated 'Loading' / 'Cargando'. Override with
< / li >
< code > aria-label="Loading orders"< / code > for context.
< / ul >
< / td >
< / tr >
< tr >
< td class = "name" > Track decoration< / td >
< td >
< code > data-spinner-track< / code > , < code > -dot< / code > , < code > -bar< / code > all carry
< code > aria-hidden="true"< / code > . Only the Provider's label is announced.
< / td >
< / tr >
< tr >
< td class = "name" > Keyboard< / td >
< td > Spinner is not focusable. Tab order skips it entirely.< / td >
< / tr >
< tr >
< td class = "name" > Focus visible< / td >
< td > Spinner does not paint a focus ring — no interactive state.< / td >
< / tr >
< tr >
< td class = "name" > Reduced motion< / td >
< td >
All three keyframes stop under
< code > prefers-reduced-motion: reduce< / code > . The visual stays so the user still
sees the loading affordance.
< / td >
< / tr >
< tr >
< td class = "name" > When to use< / td >
< td >
Spinner for unknown progress (network requests, async rendering). Switch to
< a href = "/uix/components/progress" > < Progress> < / a > when you can report a
percentage.
< / td >
< / tr >
< / tbody >
< / table >
< / div >
< / section >
< / section >
{ /if }
{ /if }
< / div >
< / div >