f-ui
Design

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

SemanticComponentWhen
EmptyEmptySuccess, no content (first-use, empty list body, filtered-out with host copy, empty panel)
ResultResultPost-submit outcome; HTTP-style 403 / 404 / 500; load failure (status="error" + Retry in extra)
LoadingSkeleton (default) / Spin (exception)No usable data yet
PopulatedHost contentData 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:

AxisLoadingEmptyResult
In the status matrixPeer cell: pending, no usable data yetPeer cell: success, no contentPeer cell: failure / exception page / op outcome
Product categoryNot a third status-block peer; split Skeleton (shape placeholder) + Spin/Spinner (process) + Progress when measurableStatus block EmptyStatus block Result
page | regionScope/density: page = content-band skeleton; region = in-card skeleton. Fullscreen Spin = app lock — not Empty/Result sizesize already shippedsize already shipped
Host branch!hasUsableData && pending → Skeleton…success && emptyerror / 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                               → Populated

Page 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).

ScenarioUse
List / Detail Hub / Form initial load, shape knownPage skeleton (replace content band)
Hub child Related List / card regionRegion skeleton (in-card / section pattern)
Shape unknown / honest “working” fallbackCentered 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 actionInline / 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.

SizeUse
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.

StateHeight-bounded bodyContent-sized / embedded
Emptyflex-1 + vertical centermin-h-48 floor, chrome centered in that band
Errorflex-1 + vertical centermin-h-48 floor, chrome centered in that band
LoadingSkeleton from the top of the bodyNatural 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

ScenarioUse
Generic panel / page “nothing here yet”Empty
QueryList / Table empty (filters + Clear)DataListEmpty (list chrome — keep API)
Chat welcome emptyMessageScrollerEmpty (chat chrome — keep API)
Submit success / failureResult
Route / ACL 403 / 404 / 500Result (exception statuses)
Fetch / load failureResult status="error" + Retry in extra
No usable data yet (shape known)Page or region Skeleton
Spinner-only blank page as CRUD defaultPage 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 notDo instead
Use Result as empty (status="info" or any status)Empty
Show Empty while pendingSkeleton / family loading
Style Error as Empty (no recovery)Result error + Retry (or family Error chrome)
Invent a kit-level triad switcherHost if over Empty + Result + Skeleton
Spinner-only blank page as CRUD defaultPage-shaped Skeleton for List / Hub / Form
Put flex-1 on Empty / Result by defaultLet the list / region container grow; Empty / Result stay content-sized unless the host opts in

Citations

See Also

On this page