Patterns
Async state
The bordered loading panel from the business dashboard — spinner plus message, role="status". The recipe is asyncState(); strings and recovery actions stay in the app. Empty and error share the same panel shell and follow the slot contract on Empty, loading, error.
Loading
Loading events…
Use when the panel shape is not yet known — a tab fetch, a workspace bootstrap. When the layout is known (a table, search results), use Skeleton instead so nothing jumps when data lands.
Empty and error
Same panel() shell. Empty is neutral; error is destructive only in copy and recovery — the panel border stays the system hairline.
No events yet
Publish an event and athletes can start rating it the same day.
Reviews didn't load
The connection dropped partway through. Nothing was lost.
Two loading contexts
| Context | Recipe | Why |
|---|---|---|
| Action in progress (submit, publish) | buttonSpinner({ size: 'button' }) + aria-busy | 16px inside the control. Label does not change. |
| Async panel (tab, page bootstrap) | asyncState() loading slots | 20px beside a message. role="status", aria-live="polite". |
| List, table, search results | skeleton() | Placeholder matches the geometry of what is coming. No spinner. |
Structure
import { asyncState, buttonSpinner } from '@sportingscouter/ui';
const s = asyncState({ inset: true });
{/* Loading — business dashboard pattern */}
<div class={s.panel()} role="status" aria-live="polite">
<div class={s.loadingRow()}>
<span class={buttonSpinner({ size: 'status' })} aria-hidden />
<span class={s.loadingMessage()}>{t('common.loading')}</span>
</div>
</div>
{/* Empty */}
<section class={s.panel()} aria-labelledby="events-empty-heading">
<h2 id="events-empty-heading" class={s.emptyTitle()}>{t('events.empty.title')}</h2>
<p class={s.emptyBody()}>{t('events.empty.body')}</p>
<div class={s.emptyAction()}>…</div>
</section>
{/* Error */}
<div class={s.panel()} role="alert">
<h2 class={s.errorTitle()}>{t('events.error.title')}</h2>
<p class={s.errorBody()}>{t('events.error.body')}</p>
<div class={s.errorAction()}>…</div>
</div>Props
| Variant | Type | Default | Notes |
|---|---|---|---|
inset | boolean | true | mt-8 top margin — matches business tab panels below a heading |
Accessibility
- Loading:
role="status"andaria-live="polite"on the panel root. Spinner isaria-hidden; the message carries the announcement. - Empty: real heading with
aria-labelledbyon the<section>. - Error:
role="alert"on the panel root. Recovery action is required — see states contract. - Never disable the whole page during loading. The chrome stays interactive; only the data region waits.
See also: Empty, loading, error · Skeleton · Button (loading on controls)
Sporting Scouter Design System
Every token on this site is generated from
theme.css.