PageSkeleton
Hero example
When to use
Drop a PageSkeleton into a Next.js loading.tsx so the route's eventual content shape is visible while server actions stream. The result reads as a recognisable page outline, not a generic spinner — the user sees that something is loading and roughly what shape the answer will take.
PageSkeleton renders the content area only. It does not include a Header — the header belongs to the surrounding shell. In apps/app, the PageLoading helper pairs PageSkeleton with the dashboard Header for a one-line route fallback.
Anatomy
A single <output> element (which carries implicit role="status") with data-layout="<preset>". Inside, three blocks compose in order:
- Header block — a title (either text from the
titleprop or a 28-pixel Skeleton) plus a 14-pixel subtitle Skeleton. - Stats row — optional. Renders
headerStatsSkeleton tiles when the value is greater than zero. - Body — the layout-preset composition. List rows, grid tiles, two-pane columns, table rows, and so on.
Every Skeleton inside is aria-hidden. The outer wrapper carries the single role="status" + aria-busy="true" so assistive tech announces the load once per page.
Layouts
Eight presets cover the dashboard's content shapes:
- list — Inbox, Timeline, Tasks, Audit log, Transactions, Sweeps. Stacked label-meta-chip rows.
- grid — Equity tiles, Banking cards, People cards. N by M tiles.
- detail — equity, board meeting, stakeholder profile. Title row, then a card-grid plus an aside.
- two-pane — Documents. Left tree column plus right grid of thumbnails.
- three-pane — Mail. Folders column, threads column, thread detail column.
- tabs-content — Entities, Board, Equity overview. A pill strip plus a card body.
- form — Settings sub-pages,
/newflows. Stacked label plus input pairs plus an actions footer. - console — Developers, Admin, Registrations. Filter bar plus a table.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| layoutrequired | PageSkeletonLayout | — | — |
| rows | number | — | Row count for list / form / console. Default tuned per layout. |
| columns | number | — | Column count for grid. Default 4. |
| tabCount | number | — | Tab pill count for tabs-content. Default 4. |
| headerStats | number | 0 | Header stats row above the main content (used on equity/banking-shaped pages). Renders N stat-tile skeletons. Default 0 (no stats row). |
| title | string | — | Page title to render as text instead of a skeleton block. Improves perceived navigation feedback — the title is known synchronously. |
| subtitleWidth | number | 380 | Subtitle skeleton width. Default 380. |
| className | string | — | — |
Also accepts Omit<React.HTMLAttributes<HTMLOutputElement>, "children">.
States
PageSkeleton has one state — loading. Once data resolves, the parent Suspense boundary unmounts PageSkeleton and renders the real content. No transition props or fade-out logic are needed; the App Router handles the swap.
Themes
Tokens consumed
--stone-200,--stone-300via the underlying Skeleton--border-soft,--bg-card,--bg-subtlefor the surrounding card and table chrome--radius-lg,--radius-mdfor card and field shapes--fg-headingwhen atitlestring is passed
Accessibility
- Keyboard interactions. None. PageSkeleton is presentational.
- ARIA roles and properties. The outer wrapper is
role="status"witharia-busy="true"andaria-label="Loading…". Child Skeletons arearia-hiddenso the load announces exactly once per page. - Live region. PageSkeleton does not use
aria-live— it is a static status region, not a polite announcement queue. Screen readers describe it on focus, which fires when the user navigates into the route. - Reduced motion. Each child Skeleton respects
prefers-reduced-motion: reduce. Shimmer sweeps stop; tracks remain visible. - Forced colors. Tracks map to
GrayText; gradient sweeps are hidden. The surrounding card chrome falls back to system colors.
Do / Don't
title when the page title is known synchronously (most authenticated routes). The user sees real navigation feedback instead of a placeholder block.loading.tsx.Recipes
Inbox-style list
import { PageSkeleton } from "@matter/components";
export default function InboxLoading() {
return <PageSkeleton layout="list" rows={8} title="Inbox" />;
}Equity overview with stats and tabs
import { PageSkeleton } from "@matter/components";
export default function EquityLoading() {
return (
<PageSkeleton
headerStats={4}
layout="tabs-content"
tabCount={8}
title="Equity"
/>
);
}Mail three-pane
import { PageSkeleton } from "@matter/components";
export default function MailLoading() {
return <PageSkeleton layout="three-pane" rows={8} title="Mail" />;
}Source
packages/components/src/PageSkeletonSkeleton
Single-block shimmer placeholder. The atom that PageSkeleton, CardSkeleton, and route-level loading.tsx files all compose from.
Card
Opaque elevated surface — bg-elev + hairline border + radius-lg. The default container for content blocks anywhere in the dashboard or marketing surfaces. Pass `padding` to override the 32px default.