C3 — Empty, Loading & Error State Catalog
Scope: Named UI patterns for every empty, loading, and error state across all Ghasi Melmastoon surfaces. Each pattern entry defines: when to use it, anatomy, variants, component props, accessibility notes, and an illustration slot for design handoff.
Rule: Every data-driven surface MUST render one of these patterns — never a blank white screen or unhandled promise rejection. Pick the closest match; if none fits, add a new entry here before shipping.
1. Loading patterns
L-01 Skeleton screen
When: Initial page/screen load where content shape is known and predictable. Always prefer over a full-page spinner.
Anatomy:
- Shimmering rectangle placeholders that match the shape of the real content (card, table row, text block).
- Shimmer gradient:
from: surface → surfaceMuted → surface, animated 1.4 s linear infinite. - No text inside skeleton elements.
Variants:
| Variant | Use case |
|---|---|
card | Property card, reservation card, task card |
table-row | Arrivals board, housekeeping board table |
detail-panel | Property detail, folio panel, room detail |
list-item | Staff list, rate plan list, notification list |
kpi-tile | Dashboard KPI tiles |
Props (component <Skeleton>):
<Skeleton variant="card" count={3} animate />
<Skeleton variant="table-row" count={10} animate />
A11y: Wrap skeleton group in <div role="status" aria-label="Loading…" aria-live="polite">. Remove once data loads.
Illustration slot: [Design handoff: Figma → Skeletons page]
L-02 Inline spinner
When: Short in-place loading (button submit, inline section refresh). Duration expected < 1.5 s.
Anatomy:
- 20px circular indeterminate spinner using
border-top: currentColor. - Placed at the centroid of the element it replaces.
- Label: screen-reader-only
aria-label="Loading".
Variants: sm (16 px), md (20 px), lg (32 px).
Do / Don't:
- ✅ Use inside a button to replace the label while submitting.
- ❌ Do not use for full-page or large section loads — use L-01.
L-03 Progress bar (determinate)
When: File upload, report export, sync progress, onboarding multi-step with known total.
Anatomy:
- Thin bar (4 px height) at the top of the panel or full viewport width.
- Fills left-to-right (LTR) / right-to-left (RTL) using
direction-awareinset-inline-start. - Color:
primarytoken. - Accessible:
role="progressbar" aria-valuenow={pct} aria-valuemin={0} aria-valuemax={100} aria-label="Upload progress".
L-04 Skeleton + stale data indicator
When: Background refresh on data that is already showing (e.g., housekeeping board polling every 30 s). Show existing data; overlay a subtle refresh indicator in the corner.
Anatomy:
- Existing content remains visible.
- Small pulsing dot or spinner in the top-right of the panel.
aria-live="polite"region announces "Refreshing…" then "Updated" when done.
2. Empty state patterns
E-01 Zero-results search
When: Search returns 0 results.
Anatomy:
- Illustration: hospitality-themed (magnifying glass over a guesthouse, custom SVG from illustration set).
- Headline (bold): "No properties found"
- Sub-text: "Try adjusting your dates, location, or filters."
- CTA: "Clear all filters" (if filters are active).
Variants:
| Variant | Headline | Sub-text | CTA |
|---|---|---|---|
search-no-results | "No properties found" | "Try adjusting…" | "Clear filters" |
map-no-results | "No properties in this area" | "Zoom out or search by name" | "Reset map" |
shortlist-empty | "Your shortlist is empty" | "Save properties to compare later" | "Discover properties" |
A11y: <section aria-label="No results">.
E-02 Empty list / board
When: A data list (arrivals board, housekeeping board, folio charges, maintenance queue) has no items.
Anatomy:
- Small illustration or icon.
- Headline: contextual ("No arrivals today", "All rooms clean", "No charges yet").
- Optional CTA: only if an action is directly possible from here.
Variants by context:
| Context | Headline | CTA |
|---|---|---|
| Arrivals board | "No arrivals today" | — |
| Housekeeping board | "All rooms are clean" | — |
| Folio charges | "No charges added yet" | "Add charge" |
| Maintenance queue | "No open tickets" | "Log issue" |
| Notifications | "You're all caught up" | — |
E-03 Offline-unavailable data
When: Data exists but cannot be loaded because the desktop is offline and the data is not in the local SQLite cache.
Anatomy:
- Icon: cloud with slash.
- Headline: "Not available offline"
- Sub-text: "This data will load when you reconnect. Last sync: <time>."
- No retry CTA (reconnect is the fix, not user action).
E-04 Permission-gated empty
When: User lands on a section they can access the URL for but lacks the role to see the content.
Anatomy:
- Lock icon illustration.
- Headline: "You don't have access to this section"
- Sub-text: "Ask your manager to update your permissions."
- No action CTA.
3. Error state patterns
ER-01 Toast / snackbar
When: Transient recoverable error; does not block workflow.
Anatomy:
- Fixed-position bottom-left (LTR) / bottom-right (RTL), 16 px from edge.
- Max-width 360 px.
- Icon:
error(red) orwarning(amber) from@ghasi/icons. - Text:
userMessageKeystring, max 2 lines. - Close button: always present.
- Optional action button (e.g., "Retry", "Undo").
- Auto-dismiss: 5 s (informational), 8 s (with action). Paused on hover/focus.
Stacking: Max 3 toasts; oldest dismissed first.
A11y: role="alert" for error, role="status" for info. Focus stays on triggering element.
ER-02 Inline field error
When: Form validation (client-side or server-side via errors[] array).
Anatomy:
- Red text below the field input, 12 px.
- Icon:
alert-circle14 px before the text. - Field border:
--color-errortoken. - Field ARIA:
aria-invalid="true"+aria-describedby="<error-id>".
ER-03 Inline section banner
When: A panel or section has a recoverable non-blocking issue (e.g., sync warning, outdated pricing, stop-sell notice).
Anatomy:
- Full-width banner inside the section,
surfaceMutedbackground with left border--color-warning. - Icon + text + optional CTA.
- Dismissible via ✕ button if not critical.
Variants: info, warning, error.
ER-04 Skeleton + retry button
When: A panel/card failed to load its data; content shape is known.
Anatomy:
- Skeleton placeholder (L-01) with a "Could not load — Retry" overlay or inline button after skeleton.
- After 2 auto-retries, show retry button.
ER-05 Modal error dialog
When: Action failed in a way that requires user decision (e.g., hold expired, folio locked).
Anatomy:
- Standard
<Dialog>from Radix UI. - Icon:
alert-triangle(error severity). - Title: error title.
- Body: human-readable explanation + what the user can do.
- Actions: primary CTA ("Try again", "Start over") + optional "Cancel".
- Dismissible only via explicit CTA.
A11y: role="alertdialog", focus trapped inside, ESC closes if Cancel is present.
ER-06 Full-screen error page
When: Unrecoverable state: session revoked, tenant suspended, device not bound, state corrupt.
Anatomy:
- Centered layout, max-width 480 px.
- Illustration: relevant (lock, cloud-offline, warning).
- Headline (H1).
- Body text (1-3 sentences).
- Primary CTA ("Log in again", "Contact support").
- Trace ID in collapsed
<details>for support.
Navigation: All nav is hidden or disabled; user cannot browse away from this page to other app areas.
ER-07 Offline indicator
When: Desktop app loses network connectivity.
Anatomy:
- Sticky banner at the top of the app shell: amber background.
- Icon:
wifi-off. - Text: "You're offline — working from local data. Last synced <relative time>."
- Auto-dismissed when connectivity resumes; replaced by "Back online — syncing…" toast for 3 s.
4. Decision tree
Request completes?
├─ No (network/timeout) → retriable? → yes → Silent retry (L-02 spinner) → after cap → ER-01 toast
│ → no → offline? → ER-07 + E-03 | ER-01 toast
└─ Yes
├─ 2xx → has data?
│ ├─ yes → render content
│ └─ no → pick E-01/E-02/E-03/E-04
└─ 4xx/5xx → look up C2 → pick ER-01/ER-02/ER-03/ER-04/ER-05/ER-06
5. Open Questions
- Should "All rooms clean" (E-02 housekeeping board) include a motivational message (hospitality warmth) or keep it factual?
- Define exact illustration set for E-01/E-02/E-04/ER-06 — blocked on design-ops/C11-iconography completion.
- Desktop offline indicator: should it show a live counter of outbox items waiting to sync?