BridgeHandbookDraft
For agents
Menu
ExperimentalutilityData

DataState

Loading, empty and error for every data region. The frame stays; only the body changes.

import { … } from "@brdgapp/ui/components/data-state"

Usage

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.

When to use

  • Any region that fetches data: a table, a list, a stat tile, a card body.
  • Inside the region's frame, after its title and before its content.

When not to use

  • For a button whose action is running. Use the Button's loading. Use Button instead.
  • Around a whole page. Each region gets its own DataState; the page frame renders straight away.

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.

Anatomy

Recent intros

No intros yet

Intros you make or receive show up here.

  • Priya Natarajan → Leah Okonkwo

    4 days ago

    Accepted

The frame stays; the body is one of the four.

  1. Loading body. The skeleton you pass: the same size and shape as the loaded content.

  2. Empty body. Soft icon circle, a bold title, a line saying what will appear here, and an optional action.

  3. Error body. "Couldn't load {what}", with Try again when onRetry is given.

  4. Loaded body. Your content, from the children function. Only called with data that isn't empty.

Examples

All states, live

Use Demo data in the sidebar: make it slow, make it fail, or replay loading.

Recent intros

Empty

Say what will appear here, and offer the next step.

Recent intros

Couldn't load

The card stays; the body offers Try again.

Recent intros

States

LoadingEmptyErrorReady
Recent intros

Recent intros

Recent intros

No intros yet

Intros you make or receive show up here.

Recent intros

Recent intros

  • Priya Natarajan → Leah Okonkwo

    4 days ago

    Accepted
  • Mei Lin Zhou → Ravi Menon

    Sep 2

    Accepted
StateWhat happens
LoadingShows skeleton; aria-busy is set.
EmptyLoaded, but nothing to show (an empty list by default; change it with isEmpty).
ErrorFailed to load. Announced to screen readers; offers Try again.
ReadyShows your content.

Behavior

  • The wrapper's data-state attribute (loading, empty, error, ready) is there for styling and tests.
  • Changes are announced politely to screen readers; the error is announced as an alert.
  • Pass status and data straight from your data hook: SWR, a fetch helper, or the handbook's useMockQuery.

Do and don't

Do

Keep the card's title and icon outside DataState, so they show in every state.

Don't

Return null 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.

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.

Accessibility

  • Loading sets aria-busy, so screen readers wait for the content.
  • The error body has role="alert".
  • Skeletons are hidden from screen readers; put aria-hidden on your skeleton.
KeysAction
TabReaches Try again or the empty-state action when they're shown.

API reference

DataState

Chooses which body to show.

PropTypeDefaultDescription
status"loading" | "error" | "success"—Where the request is.
dataT | undefined—The loaded data.
skeletonReactNode—The loading body. Same size as the loaded content.
empty{ icon?, title, description?, action? }—The empty body's content.
errorSubjectstring—Finishes "Couldn't load …": "intros".
onRetry() => void—Shows Try again on error.
isEmpty(data: T) => booleanempty array, null or undefinedWhen loaded data counts as empty.
children(data: T) => ReactNode—The loaded body.
classNamestring—Layout only.

Lineage

What it's built from and where it's used, read from the code. When you change it, check these too.

Built from
Button
Used by components
None

Known gaps

Where the code still differs from the system. Follow the system, not the gap.

  • New in the package. Bridge OS has no shared wrapper yet: each card writes its own loading, empty and error (with shared skeletons such as DataTableSkeleton). They move onto DataState as Bridge OS adopts the package.

Sources

  • Decision 2026-11-02: one shared wrapper (#15); each region loads on its own (#14).
  • Bridge OS DESIGN.md → principles 5–7 (motivating empty states; a card never unmounts for missing data; no layout shift).
  • Bridge OS error copy ("Couldn't load introductions", "Couldn't load this profile").