Agent docs

PageInfo, PageLoading, PageError

The centered icon, title, subtitle, and actions block when the page IS the message.

PageInfo is the centered icon, title, subtitle, and actions block for any "the page IS the message" moment: empty tables, error pages, upload prompts, not-found states. PageLoading and PageError, from the sibling page-states module, are the two other presets built on the same primitive, alongside EmptyState (documented separately in empty-state.md). One shape, so these surfaces never grow bespoke copies.

The canon behind it. docs/design-language/04-loading-and-states.md §A1 (04-loading-and-states.md:15, skeletons for content, spinners for processes, why PageLoading is the one sanctioned full-page spinner), §A7 (04-loading-and-states.md:33, empty states are content and branch), §A9 (04-loading-and-states.md:39, tone carries meaning, not decoration), and §A11 (04-loading-and-states.md:45, a failure looks like a failure and says what failed).

When to reach for it#

Reach for a preset first: EmptyState for nothing-here-yet, PageLoading for a still-fetching page or panel, PageError for a failed load. Reach for PageInfo directly only when a preset's narrower prop set does not fit. EmptyState forwards tone since 2026-08-14 (see empty-state.md's Contract), so a toned empty region no longer needs the raw primitive; the pre-gap call sites that reached for PageInfo to get one (walmart-mvp/frontend/src/pages/ErrorManagement.tsx:1087, tone="success", "All clear"; the muted admin-list empties in walmart-mvp/frontend/src/pages/Admin.tsx:237-668) become fold-back candidates at that app's next kit bump.

The canon's three-way empty branch (doc 4 §A7: true-empty, filtered-empty, in-progress-empty) applies to any PageInfo-based empty region, not only ones built through EmptyState. Naming which of the three a given render is, and picking tone accordingly (doc 4 §A9), is the job regardless of which entry point you used.

Contract#

PageInfo#

  • Icon (default "Info") / iconSize / iconClassName / iconWrapClassName (IconImportProps).
  • title / subtitle: string or JSX via renderNode.
  • actions: free JSX slot under the text, for whatever recovers or advances (a button, a link).
  • size ("sm" | "md" | "lg", default "md"): overall scale. sm for table/card empties, md for sections, lg for full-page states. Each size fixes its own padding, icon-disc size, and type scale together (page-info.tsx:6-13), so a caller never mixes a small disc with page-weight padding.
  • tone ("neutral" | "muted" | "info" | "success" | "warning" | "error", default "neutral"): recolors the icon disc and icon. Per doc 4 §A9, tone carries meaning, not decoration: muted when the absence is neutral, success when the absence IS the good outcome (an errors tab reporting none), neutral's own icon reads as a deliberate 30%-ink wash rather than an absent one (page-info.tsx:16-19 comment). Real usage: Admin.tsx:211, 302, 336, 619, 667 all use tone="muted" for ordinary empty lists; ErrorManagement.tsx:1087 uses tone="success" for "All clear."
  • noIconCircle: renders the bare icon with no tinted disc behind it.
  • skeleton: a placeholder block, a circle and two bars, in place of real content.

PageLoading#

Wraps PageInfo with Icon={LoadingIcon}, noIconCircle always on, and the spinner tinted text-primary (page-states.tsx:11-44). title defaults to "Loading…"; description, action, compact (maps to PageInfo's size="sm" vs "md"), spinnerSize, spinnerClassName, and skeleton round out the props. Canon doc 4 §A7 and §B name it "the one sanctioned full-page spinner": every other loading surface in the house style is a skeleton (doc 4 §A1), and PageLoading is the deliberate exception for a genuinely full-page or full-panel transient wait.

No shipped usage of PageLoading found in either app. Both apps handle their full-page loading state by branching on data == null and rendering a page-specific skeleton component instead, for example walmart-mvp/frontend/src/pages/Home.tsx:162-164 (if (data == null) return <HomeSkeleton />) and the whole routeSkeletons.tsx boot-frame module. This matches the house rule that content placeholders are skeletons (doc 4 §A1), so the gap is not a missed convention: PageLoading remains the documented preset for a full-page spinner, but the actual full-page cases observed are all covered by page-shaped skeletons instead, leaving PageLoading for a case (a full panel with no known shape to skeleton) that has not shipped yet.

PageError#

Wraps PageInfo with Icon="Error" by default, tone="error", and iconCircle on by default (iconCircle here is the inverse of PageInfo's noIconCircle, because "errors deserve weight," doc 4 §A7). title defaults to "Something went wrong"; description, action, compact, and skeleton follow the same shape as PageLoading.

Heavily used in walmart for a failed data load: walmart-mvp/frontend/src/pages/NotFound.tsx:11, ConnectLaunch.tsx:45, ActiveParts.tsx:177, Jobs.tsx:322, 882, UnlistedParts.tsx:244, ErrorManagement.tsx:920, SpecViewer.tsx:259, 348, Home.tsx:155, PartPreviewModal.tsx:575, 854. No shipped PageError usage found in speedway. Speedway is a Remix app whose loaders throw to a route-level ErrorBoundary (speedway/app/root.tsx:566), which likely explains the gap: a loader failure there is caught by the route boundary rather than rendered as an in-page PageError block.

Sanctioned combinations#

CombinationProducesWhere usedWhy
PageInfo size="sm" tone="muted" Icon={...} title={...} className="py-3"A nested, neutral empty region inside a card sectionAdmin.tsx:302, 336, 619, 667Doc 4 §B: "Empty and unremarkable → PageInfo tone=muted"
PageInfo Icon="Success" tone="success" size="lg" title="All clear" subtitle="..."A tab reporting a genuinely good absenceErrorManagement.tsx:1087Doc 4 §A9's worked case verbatim: "no errors is good news here, so it reads as an achievement, not an absence"
PageError title="Couldn't load..." description={loadError} action={<Button content="Retry" .../>}A failed-load state with a named cause and a recovery pathHome.tsx:155-160The failed-load preset (doc 4 §B, "Full-page transient... errors get PageError"); note the description is the caught error's own message, not a generic string, per doc 4 §A11
PageError title="Couldn't load this part" description={loadError}, rendered as one branch of a modal body alongside a skeleton and the loaded contentAn overlay's own failed-load state, matching the page'sPartPreviewModal.tsx:853-856Doc 10 §A11: "empty and error states inside an overlay match the page's"

Banned combinations#

Do not build a bespoke centered icon/title/subtitle block by hand for an empty, loading, or error moment. PageInfo (directly or through EmptyState/PageLoading/PageError) is the one primitive for this; every real instance in both apps routes through it.

Do not pick tone="neutral" (the default) for an absence that is actually good news, and do not pick tone="success" for an ordinary, unremarkable absence. Doc 4 §A9's check is direct: name what the absence MEANS before picking the tone, and if the answer is "the user won," it is not muted.

Do not render a bare, generically-worded error string in place of PageError's description. Doc 4 §A11 ranks "an error surface that shows the transport's raw string" and "a handler that flattens a caught exception into its own generic wording" as real, ranked failure modes; prefer the server's own description of the failure, or an id the user can quote.

Before you adopt this#

Five questions to answer before reaching for PageInfo.

  1. Does the shell, a parent layout, or a global provider already render this? Not applicable, PageInfo is page content; no shell already renders it.
  2. Does this app already ship a local implementation of the same thing? A bespoke centered icon/title/subtitle block is the local-clone trap; route through PageInfo.
  3. Does this app's kit pin reach the version this component or prop landed in? EmptyState's tone passthrough landed 2026-08-14; confirm the app's kit pin reaches it.
  4. Does the component derive its own accessible name and keyboard path, or must the call site supply them? Not applicable, action's own button or link supplies its accessible name.
  5. Which canon §A rules bind this surface, and which does the composition break? §A9 makes tone carry meaning; §A11 requires a failure to say what failed.

Travels with#

EmptyState, the nothing-here-yet preset over the same primitive, documented in empty-state.md; that doc also names PageError as EmptyState's sibling for a failed load rather than a genuine absence.

IconCycle's animated icons, shaped for the same Icon slot, for in-progress states passed to PageInfo or EmptyState alike.

Card, whose own skeleton mode is the box-preserving alternative when the loading surface has a known card shape to keep, as opposed to PageLoading/PageInfo skeleton's content-shaped placeholder.

Snippet#

// walmart-mvp/frontend/src/pages/ErrorManagement.tsx:1087 (tone-carrying empty region)
<PageInfo
Icon="Success"
tone="success"
size="lg"
title="All clear"
subtitle="Nothing open matches these filters."
/>
// walmart-mvp/frontend/src/pages/Home.tsx:155-160 (failed load)
<PageError
title="Couldn't load the dashboard"
description={loadError}
action={<Button size="sm" content="Retry" onClick={() => window.location.reload()} />}
/>
@versable-git/ui · reference, canon, and method, read in place