Three states
Generated from DESIGN_SYSTEM.md
Rules
Every view that loads data renders exactly one of four states: loading, empty, error, or its content. The first three are designed, never left to chance: a surface that shows nothing while it waits, or shows nothing when it failed, has left the person to guess which of four things is true.
| State | What it shows | Never |
|---|---|---|
| Loading | A skeleton of the loaded shape (Skeleton / Loading below) | A blank area, or a lone spinner over a full screen |
| Empty | An icon, a descriptive title and the action that fills it (Empty and Error States) | A blank panel, or "No data" with no next step |
| Error | An inline message with a retry, at the smallest part that failed | A full-screen block for a recoverable error, or silence |
| Content | The data | — |
- Empty and error are different facts. "There is nothing yet" and "we could not load it" never share a message or a look; a failed load that renders as empty tells the person their data is gone.
- The failure stays where it happened. A widget, card or row that fails shows its own error state; the page around it keeps working.
- Loading appears fast and holds its shape. Within 100ms, at the loaded size, so nothing jumps when the content arrives.
- Each platform handles all three on every data view: web
useQueryrendersisLoading, empty andisErrorexplicitly; iOS and Android view models expose the same four cases.
Skeleton / Loading
- Loading state uses skeleton that matches the loaded shape (same height, same column layout)
- Animate with shimmer/pulse — never a spinner alone for full-screen loads
- Appear within 100ms of initiating the load
Web: animate-pulse Tailwind + matching div shapes iOS: .redacted(reason: .placeholder) + .shimmering(active: isLoading) Android: ShimmerEffect modifier / Placeholder composable
Live specimens
Reading list
Loading
Reading list
Your reading list is empty
Reading list
Could not load your reading list.
Reading list
- One focal point per screen
- Why motion explains change