Page And Region Status
Choose Empty, Result, or Skeleton for page and region status — never invent a third peer or use Result as empty.
Use this page when a page or region must show Loading, Empty, Error, or Populated after an async fetch (or a post-submit / exception outcome). The industry axis is Empty + Result + Skeleton with a host branch — not a kit-level triad switcher.
Loading is a matrix sibling of Empty and Result (pending / no usable data yet). It is not a third peer status-block product with Empty/Result-like title / description / action / size — compose Skeleton, Spin/Spinner, and Progress instead.
Decision Matrix
| Semantic | Component | When |
|---|---|---|
| Empty | Empty | Success, no content (first-use, empty list body, filtered-out with host copy, empty panel) |
| Result | Result | Post-submit outcome; HTTP-style 403 / 404 / 500; load failure (status="error" + Retry in extra) |
| Loading | Skeleton (default) / Spin (exception) | No usable data yet |
| Populated | Host content | Data present |
Forbidden: Result status="info" (or any Result status) as a substitute for Empty. Do not invent a kit Loading block peer to Empty/Result.
Same Matrix, Different Product Category
Loading shares the async status matrix with Empty and Result, but lands in a different product category:
| Axis | Loading | Empty | Result |
|---|---|---|---|
| In the status matrix | Peer cell: pending, no usable data yet | Peer cell: success, no content | Peer cell: failure / exception page / op outcome |
| Product category | Not a third status-block peer; split Skeleton (shape placeholder) + Spin/Spinner (process) + Progress when measurable | Status block Empty | Status block Result |
| page | region | Scope/density: page = content-band skeleton; region = in-card skeleton. Fullscreen Spin = app lock — not Empty/Result size | size already shipped | size already shipped |
| Host branch | !hasUsableData && pending → Skeleton… | success && empty | error / 403/404/500 / submit Result |
Forbidden: resurrect AsyncState; invent Loading with Empty/Result-like props as a kit product.
Host Branch Order
The container that owns the async boundary branches explicitly:
if (!hasUsableData && pending) → Loading (Skeleton; Spin only for exceptions)
else if (!hasUsableData && failed) → Load failure (Result error + Retry)
else if (success && empty) → Empty
else if (ACL / missing / route) → Result 403 | 404 | 500
else if (post-submit outcome) → Result success | error | …
else → PopulatedPage vs region for Loading: page surfaces replace the content band with a page-shaped skeleton; region surfaces use an in-card / in-panel skeleton (or family loading chrome). Do not wipe an entire app shell unless the wait is a true app lock.
Page Loading Rules
First entry to a page with a predictable layout → page-shaped Skeleton that mirrors the real chrome (title/toolbar, stats or filter band, table/cards/form fields). Never frame-only (shell chrome with an empty content void).
| Scenario | Use |
|---|---|
| List / Detail Hub / Form initial load, shape known | Page skeleton (replace content band) |
| Hub child Related List / card region | Region skeleton (in-card / section pattern) |
| Shape unknown / honest “working” fallback | Centered Spin (+ short tip if wait may exceed ~1s) |
| Whole app locked (auth hydrate, blocking submit occupying UI) | Spin fullscreen — exception, not CRUD default |
| Button / form submit / discrete action | Inline / button Spin — not page skeleton |
| Measurable long job (upload, etc.) | Progress — not skeleton |
Initial vs refetch vs route remount
- Initial: no usable data → page or region skeleton (or Spin exception above).
- Refetch: keep stale content; subtle progress (e.g. DataList body overlay) — do not wipe the page into skeleton.
- Refetch failure: keep content + inline recovery — never collapse into Empty.
- Route remount: a sibling object-page URL that unmounts the list is not a first visit. If the query cache already has rows, show them. Skeleton only when there is no usable data. Host SPAs keep the list instance mounted (hide +
inert); QueryList does not own keep-alive.
Hosts may delay showing loading chrome (~100–400ms). Sub-second flashes of skeleton can feel worse than a brief blank; no dedicated delay component is required.
Page Vs Region
Empty.size and Result.size are "page" | "region" — layout density only, not a product category.
| Size | Use |
|---|---|
region (Empty default) | Card / panel / section inside a populated page |
page (Result default) | Full-page band inside PageContainer (surface="plain" when showcase-relevant) |
Loading scope is separate: page skeleton vs region skeleton vs fullscreen Spin. Fullscreen Spin is an app-lock exception — it is not an Empty/Result size variant.
Container Placement
size does not place the block in the parent. Do not put flex-1 on Empty / Result by default — that steals leftover height from a page flex column and opens a blank band between siblings. Height-bounded list containers (QueryList / Table fillHeight, Related List body) apply flex-1 themselves. Loading skeleton stays top-aligned.
| State | Height-bounded body | Content-sized / embedded |
|---|---|---|
| Empty | flex-1 + vertical center | min-h-48 floor, chrome centered in that band |
| Error | flex-1 + vertical center | min-h-48 floor, chrome centered in that band |
| Loading | Skeleton from the top of the body | Natural height of N skeleton rows |
Do not leave Empty / Result hugging the toolbar in a tall table card. Do not vertically center skeleton rows.
Examples
Page Skeleton — List
First paint for a resource index: toolbar, optional stats band, and table row blocks — not a spinner in a blank card.
"use client";
import { ListPageSkeleton } from "./page-skeletons";
export function PageSkeletonListDemo() {
return <ListPageSkeleton />;
}Page Skeleton — Detail Hub
Object Page Hub hydrate: page header, descriptions band, and section cards as a content-band wireframe while the record loads.
"use client";
import { HubPageSkeleton } from "./page-skeletons";
export function PageSkeletonHubDemo() {
return <HubPageSkeleton />;
}Page Skeleton — Form
Create/edit first paint mirrors field groups. For the same-instance hydrate path, prefer Form Page loading, which renders a field skeleton and suppresses the footer until values land.
"use client";
import { FormPageShapeSkeleton } from "./page-skeletons";
export function PageSkeletonFormDemo() {
return <FormPageShapeSkeleton />;
}Spin Exception
Centered or fullscreen Spin is for unknown shape, honest “working” fallback, or whole-app lock — not the List / Detail Hub / Form CRUD default. Hosts should use the kit component (not a hand-rolled CSS ring).
Loading
"use client";
import { PageSpinException } from "./page-skeletons";
export function PageSpinExceptionDemo() {
return <PageSpinException />;
}Boundaries
| Scenario | Use |
|---|---|
| Generic panel / page “nothing here yet” | Empty |
| QueryList / Table empty (filters + Clear) | DataListEmpty (list chrome — keep API) |
| Chat welcome empty | MessageScrollerEmpty (chat chrome — keep API) |
| Submit success / failure | Result |
| Route / ACL 403 / 404 / 500 | Result (exception statuses) |
| Fetch / load failure | Result status="error" + Retry in extra |
| No usable data yet (shape known) | Page or region Skeleton |
| Spinner-only blank page as CRUD default | Page skeleton instead |
| Frame-only skeleton (chrome + empty void) | Content wireframe that mirrors real layout |
Family empties (DataListEmpty, MessageScrollerEmpty) stay owned by their hosts. Optional internal layout reuse of Empty is a follow-up — not required.
Antipatterns
| Do not | Do instead |
|---|---|
Use Result as empty (status="info" or any status) | Empty |
| Show Empty while pending | Skeleton / family loading |
| Style Error as Empty (no recovery) | Result error + Retry (or family Error chrome) |
| Invent a kit-level triad switcher | Host if over Empty + Result + Skeleton |
| Spinner-only blank page as CRUD default | Page-shaped Skeleton for List / Hub / Form |
Put flex-1 on Empty / Result by default | Let the list / region container grow; Empty / Result stay content-sized unless the host opts in |
Citations
- Ant Design Empty; Empty Status research
- Ant Design Result; Result Page; Exception Page
- Ant Design Skeleton; Ant Design Spin
- Nielsen Norman Group — Empty states
- Nielsen Norman Group — Skeleton Screens 101
- Carbon — Empty states
- Carbon — Loading pattern
- Polaris EmptyState
See Also
- Spin — unknown-shape wait, nested refresh overlay, fullscreen app lock
- Empty — successful no-content
- Result — outcomes, exceptions, load failure
- Object Messaging And Table Chrome — Message Popover, cell value state, region toolbar
- Form Page — field skeleton via
loadingon edit hydrate - CRUD Page Patterns — list / detail / form surfaces; Edit: Loading the Record
- List Page Statistics — stats band loading alongside list chrome
- Showcases: /showcases/orders-list, /showcases/orders-detail, /showcases/orders-form-edit
Approval And Case Patterns
Compose multi-party approvals and complaint case workbenches from f-ui page recipes — including the Role Surface Formula for different viewers on the same instance.
Form Layout
Choose vertical fields, two-column field grids, control widths, and FormActions placement — without confusing “two columns” with Ant labelCol.