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.

Two loading contexts

ContextRecipeWhy
Action in progress (submit, publish)buttonSpinner({ size: 'button' }) + aria-busy16px inside the control. Label does not change.
Async panel (tab, page bootstrap)asyncState() loading slots20px beside a message. role="status", aria-live="polite".
List, table, search resultsskeleton()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

VariantTypeDefaultNotes
insetbooleantruemt-8 top margin — matches business tab panels below a heading

Accessibility

  • Loading: role="status" and aria-live="polite" on the panel root. Spinner is aria-hidden; the message carries the announcement.
  • Empty: real heading with aria-labelledby on 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.