Loading
Render what the screen already knows, load slow parts on their own, and hold skeletons shaped like the finished UI.
A loading screen is the finished screen with some values still on the way. Draw everything the screen already knows at once, fetch slow parts in requests of their own, and hold a Skeleton in each spot a value is still coming, sized like that value. When the data lands, values replace bars and nothing else on the screen moves.
When to use
Any screen that waits on the network before it can show its content: a record page, a detail panel, a tab body, a card on a dashboard.
Show what you already know
Render anything that needs no network straight away:
- The page chrome — header, breadcrumbs, tab bar and its icons, section headings, field labels, empty frames and their borders.
- Route params — the open tab, the selected home, the record's id.
- Data carried from the previous screen — the row someone clicked already holds the customer's name, so the link carries it in history state and the record's header shows it as text while the rest loads. A record opened with no carried name, from a bookmark or a pasted link, shows a bar there like any other value, and the value the read returns replaces the carried one.
Hold a skeleton only where a value is still coming. The customer record in Pro Web and Admin draws its header, tab bar, Overview section headings and contact labels while its read is in flight, with bars for the address, the contact values and each section's body.
Split slow reads out
When one part of a screen is much slower than the rest, fetch it in a request of its own so the fast part renders first. On the Contractor API, one request waits on its parts in turn while separate requests run side by side, so a split read shows each part as soon as that part answers.
Pro Web's customer record reads the person and contacts, the homes, and the loyalty and voucher ledgers as three requests sent together. Staging medians for one customer, timing the person-and-contacts part and the homes part:
The person shows at about 300 ms, down from about 770 ms, and the person and homes together land no later than the single read.
Each request owns its own states:
- Loading — a skeleton in the region that request fills, and only there. The rest of the screen stays live.
- Error — the failure shows in the region that request fills, with the parts that loaded still on screen.
- Retry — re-sends that request alone. The parts already in hand stay put.
Measure before you split. Time each part on staging, and split along the boundary where the times differ. Splitting pays only when the slow part can be selected on its own: a field added to the fast read can pull the slow work back into it, so time the fast read again whenever its selection changes.
Skeletons match the finished UI
A skeleton is the loaded layout with bars standing in for values:
- Same layout — the same containers, borders, insets, gaps and grid as the loaded state. Build the skeleton from the loaded component's own frame where you can.
- Real text where the text is known — headings, field labels, tab names and button labels render as themselves.
- Bars only where values go — one bar per value, sized like the content it stands in for: as wide as a typical value and as tall as the text's glyphs, centered in that text's line box so the row keeps its loaded height.
- Nothing moves when data lands — the loaded screen draws over the skeleton pixel for pixel; only bars become values.
Mark the loading region aria-busy="true" and give it an sr-only line that
says what is loading, so a screen reader moving through the region hears what
the bars stand for.
Usage
import { SkeletonText } from "@smartacteam/ambient-web/skeleton";SkeletonText is a bar for one line of text: a box one line tall with a bar
one em tall centered in it, both read from the surrounding font size and line
height. Its className sets the width.
<div aria-busy={loading || undefined}>
{loading && <p className="sr-only">Loading customer…</p>}
<dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm leading-5">
<dt className="text-text-secondary">Email</dt>
<dd>{loading ? <SkeletonText className="w-32" /> : customer.email}</dd>
</dl>
</div>Do and don't
Do
- Give each request its own skeleton, error state and retry.
- Keep the page's chrome on screen while it loads.
- Size each bar like the value it stands in for.
Don't
- Fail the whole page, or retry every request, when one part fails.
- Cover the page with a spinner.
- Fill a region of mixed content with one generic gray box.