BridgeHandbookDraft
For agents
Menu
Draft

Loading, empty and error

How every data region shows it's loading, empty or failed.

Every piece of a screen that fetches data shows one of four things: loading, empty, failed or the data. The page frame appears straight away; each part loads on its own inside its own frame, and nothing jumps when the data arrives.

The problem

A Bridge screen pulls from several places at once: intros, contacts, numbers. If the whole page waits for the slowest part, people stare at a blank screen. If a card hides when its data fails, people can't tell broken from empty, and a count that failed but shows 0 tells them something false.

What to do

  • Frame first, then each part. The page and every card's title show straight away. Each card loads on its own, inside its own frame, through DataState.
  • Hold the space. The skeleton is the same size and shape as what replaces it, so nothing jumps when the data arrives.
  • Every state says something. Empty says what will be here and how to fill it. Failed says "Couldn't load …" and offers Try again. Never a blank card, never a silent 0.

Which one do I use?

SituationDo thisBridge example
A list or table is loadingDataState with a skeleton of the same rows at the same height (DataTableSkeleton for tables)Recent intros: four gray rows, each as tall as a real one
A number is loading or failedStatBoxSkeleton while it loads; the error body if it fails. Never 0Outcomes → Intros tiles
Loaded, but nothing there yetDataState's empty body: what will be here, and one action if there's an obvious next stepNo intros yet. Intros you make or receive show up here.
A search finds nothingSay "No results", not the empty message: the data is there, the search missedSharing → search for a name nobody has
The data failed to loadDataState's error body, inside the same frame, with Try againCouldn't load intros
A button needs data that hasn't arrivedButtonSkeleton at the button's size. An action that's already running uses the button's own spinner insteadAny action that waits on loaded data

See it work

Live examples built from the package on the sample network. Click through them.

Each card loads on its own

The numbers arrive first, the intros later. Both titles show from the start, and nothing moves when the rows come in. Press Load again to watch it.

Your network

Recent intros

Empty, and no results

"Sharing with" has people: search for a name that isn't there to see No results. "Shared with you" has nobody yet, so it says what will appear.

Sharing

Failed: say so, in place, with Try again

This card fails the first time. The frame stays, the numbers don't turn into 0, and Try again loads it.

Your network

What to write

PartRuleExample
Empty title"No {things} yet", or what isn't there yet.No intros yet
Empty lineWhat will show up here, or how to get some. One sentence.Intros you make or receive show up here.
Empty actionOne button, only when there's a clear next step. Name the action.Make an intro
Search found nothing"No results". Not the "yet" message.No results
Error"Couldn't load {thing}". Never a raw error message or a code.Couldn't load intros
RetryAlways "Try again".Try again

Accessibility

  • While a region loads, DataState sets aria-busy, so screen readers wait for the content. Skeletons are hidden from them with aria-hidden.
  • The error body is an alert: it's read out when it appears.
  • Changes between states are announced politely, without moving focus.
  • Try again and the empty-state action are ordinary buttons, reached with Tab.
  • The error isn't shown by color alone: it has an icon and words.
KeysWhat happens
TabReaches Try again, or the empty-state action, when shown.
Enter / SpacePresses it.

Components this uses

Each opens that component’s own page, with its options and code.

ComponentIts job here
DataStateChooses the body: skeleton, empty, error or the data. Goes inside the card, under its title.
DataTableDataTableSkeleton is a table's loading body, with the loaded row height.
StatBoxStatBoxSkeleton holds a number's place until it loads.
ButtonThe empty state's one action (primary); ButtonSkeleton for a button waiting on data; DataState draws Try again itself.
SearchInputWhen it's in use, an empty list means "No results", not "nothing yet".

Rules

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.

Counts match what's shown

A number on screen comes from the same filtered list that renders the items, never from a separate count that can disagree. Never show a silent 0 when a count failed to load.

Where Bridge OS differs today

Follow this page, not the gap.

  • Bridge OS cards write their own loading, empty and error bodies; they move onto DataState as Bridge OS adopts the package.
  • Settings → Notifications draws nothing until its preferences load, and stays hidden if they fail (profile-tab.tsx, NotificationPreferencesSection: if (!prefs) return null).
  • The sharing table's failure says "Failed to load" with "Retry"; it should say "Couldn't load …" with Try again (sharing-table.tsx).
  • The Activity feed's failure says "Couldn't load activity" but offers no Try again, only "Try refreshing the page" (activity-page-content.tsx).
  • The network settings Overview tab swaps the whole tab for one skeleton while it loads, instead of each card loading on its own (overview-tab.tsx).

Sources

  • Decision 2026-11-02 (#15 shared wrapper)
  • Decision 2026-11-02 (#14)
  • Bridge OS DESIGN.md → principle 7
  • Bridge OS DESIGN.md → principle 5; ux-copywriter skill → Empty states
  • Bridge OS DESIGN.md → principle 6; components.md
  • components.md → Button / ButtonSkeleton
  • Bridge OS sharing-table.tsx (empty body: "No results" while searching, "Not sharing with anyone yet" otherwise)
  • DataState page → Accessibility and Behavior; packages/ui/src/components/data-state.tsx
  • DESIGN.md → 2. Color
  • packages/ui/src/components/data-table.tsx; StatBox page → Do / Don't