Content
- Empty title: "No {things} yet". Empty line: what the person gets, or how to get some.
- Error: "Couldn't load {things}" and "Try again". Never show a raw error message.
Loading, empty and error for every data region. The frame stays; only the body changes.
import { … } from "@brdgapp/ui/components/data-state"DataState is the one way a piece of data shows that it's loading, empty or failed. It sits inside a card or section, below its title: the frame always shows, and only the body inside DataState changes. Every table, list and data card goes through it, so they all behave the same. The usual mistakes are hiding the whole card when its data fails (error ? null), and showing one spinner for the whole page.
loading. Use Button instead.The frame stays
A card never disappears because its data is missing. Loading, empty and failed are three bodies inside the same frame (icon, title and hint stay put), and a failure offers Try again. Returning null on error deletes the feature from the page: the person can't tell broken from empty.
Each part loads on its own
The page frame shows straight away; each card, table or list shows its own skeleton, then its data. Never hold the whole page behind one if (isLoading) return <Skeleton />: a slow part would block the fast ones.
Reserve the space
Nothing that arrives later may push content down. A skeleton has the same height and shape as what it stands for, measured, not guessed. A visible jump after the first paint is a bug.
Say what happens
Buttons start with a verb and name the object: "Stop sharing", not "OK" or "Submit". A confirm dialog asks a question about the specific thing and its button repeats the verb. Errors say what happened and what to do next.
No intros yet
Intros you make or receive show up here.
Couldn’t load intros
Priya Natarajan → Leah Okonkwo
4 days ago
The frame stays; the body is one of the four.
Loading body. The skeleton you pass: the same size and shape as the loaded content.
Empty body. Soft icon circle, a bold title, a line saying what will appear here, and an optional action.
Error body. "Couldn't load {what}", with Try again when onRetry is given.
Loaded body. Your content, from the children function. Only called with data that isn't empty.
Use Demo data in the sidebar: make it slow, make it fail, or replay loading.
Say what will appear here, and offer the next step.
The card stays; the body offers Try again.
| Loading | Empty | Error | Ready | |
|---|---|---|---|---|
| Recent intros | Recent intros | Recent introsNo intros yet Intros you make or receive show up here. | Recent introsCouldn’t load intros | Recent intros
|
| State | What happens |
|---|---|
| Loading | Shows skeleton; aria-busy is set. |
| Empty | Loaded, but nothing to show (an empty list by default; change it with isEmpty). |
| Error | Failed to load. Announced to screen readers; offers Try again. |
| Ready | Shows your content. |
data-state attribute (loading, empty, error, ready) is there for styling and tests.status and data straight from your data hook: SWR, a fetch helper, or the handbook's useMockQuery.Do
Keep the card's title and icon outside DataState, so they show in every state.Don't
Returnnull when the data fails. The person can't tell broken from empty, and the feature vanishes from the page.Do
Make the skeleton match the loaded height (Bridge OS measured within 2px).Don't
Use a short stub skeleton. The jump just moves to when the data arrives.Do
Say what will be here: "Intros you make or receive show up here."Don't
Write only "No data" or show a silent 0.aria-busy, so screen readers wait for the content.role="alert".aria-hidden on your skeleton.| Keys | Action |
|---|---|
| Tab | Reaches Try again or the empty-state action when they're shown. |
DataState
Chooses which body to show.
| Prop | Type | Default | Description |
|---|---|---|---|
status | "loading" | "error" | "success" | — | Where the request is. |
data | T | undefined | — | The loaded data. |
skeleton | ReactNode | — | The loading body. Same size as the loaded content. |
empty | { icon?, title, description?, action? } | — | The empty body's content. |
errorSubject | string | — | Finishes "Couldn't load …": "intros". |
onRetry | () => void | — | Shows Try again on error. |
isEmpty | (data: T) => boolean | empty array, null or undefined | When loaded data counts as empty. |
children | (data: T) => ReactNode | — | The loaded body. |
className | string | — | Layout only. |
What it's built from and where it's used, read from the code. When you change it, check these too.
Where the code still differs from the system. Follow the system, not the gap.
DataTableSkeleton). They move onto DataState as Bridge OS adopts the package.DESIGN.md → principles 5–7 (motivating empty states; a card never unmounts for missing data; no layout shift).