BridgeHandbookDraft
For agents
Menu
ExperimentalorganismData

DataTable

The one table for product data, with its loading skeleton and pager.

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

Usage

DataTable is Bridge's one table for product data: people, organizations, intros. You describe the columns (label, icon, a comfortable width, how to draw each cell); the table handles spacing, dividers, hover, scrolling and keyboard. With DataTableSkeleton for loading and DataTablePagination for pages, it covers the whole list. The usual mistakes are passing spacing classes to fix one table, and giving a column a width too small for its content.

When to use

  • Any list of records people scan, compare or open: people, organizations, intros, invites.

When not to use

  • For a short list of cards or a feed. Use a list.
  • For admin tables with drag-to-reorder, editable cells or bulk actions. Those are a different component (not yet in the package).

One of each

Bridge has one button, one label, one menu, one tooltip, one confirm dialog and one table. Before building a control, look for it here. Never hand-make a second version (a <button> with pill classes, a <span> chip, a new kebab), even if it looks the same today: copies drift.

The look comes from props

A component's appearance comes only from its own options (variant, size, color). className on a package component is for layout (mt-4, shrink-0, w-full), never for font size, color, padding, radius or hover. If the look needs a className, the component or the chosen option is wrong: fix that instead.

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.

Numbers line up

Numbers that change or get compared (stats, counts, dates in a column, page numbers) use tabular-nums, so digits keep their width and columns line up.

One way to write dates

"Today", "Yesterday", "4 days ago" under 7 days; then "Jul 20", and "Jul 20, 2025" in another year. Times are 12-hour (9:40 AM), in the viewer's own time zone. Use formatDate and formatTime from @brdgapp/ui/lib/format.

Anatomy

Intro
Status
Made
Priya Natarajan → Leah Okonkwo
Accepted
4 days ago
Mei Lin Zhou → Ravi Menon
Accepted
Sep 2
Jonas Becker → Hannah Weiss
Requested
Yesterday
  1. Header row. Light tint, a divider below. Each header is an icon + 12px Bold label, or your own (ColumnSortButton, ColumnFilterMenu).

  2. Rows. Dividers between rows; 24px padding at the outer edges, 12px between columns.

  3. Cells. Whatever render returns; long text should truncate inside it.

  4. Pagination. Optional, under the table: range, previous, page numbers, next.

Examples

Intros

Sort by Made, change pages, open a row. Use Demo data in the sidebar to see loading and error. Try 390 to see it scroll sideways.

Intros

Click a row, or Tab to it and press Enter.

States

RestHoverFocus
Clickable row
Intro
Status
Priya Natarajan → Leah Okonkwo
Accepted
Intro
Status
Priya Natarajan → Leah Okonkwo
Accepted
Intro
Status
Priya Natarajan → Leah Okonkwo
Accepted
Sort button, sorted
Sort button, not sorted
StateWhat happens
LoadingDataTableSkeleton with the same columns, inside DataState.
Empty / errorDataState's empty and error bodies.
Row hoverFaint tint; a clickable row gets a stronger tint and a pointer.
Row focusClickable rows: the same tint and a blue inner ring.

Behavior

  • Columns fill a wide screen (each takes a share of the extra width by weight) and never shrink below minWidth; below the total, the table scrolls sideways inside its card.
  • Clickable rows (onRowClick) can be reached with Tab and opened with Enter or Space.
  • onRowPrefetch runs on hover or focus, to load what a click will need.
  • The page buttons are built on Base UI Button, like Button.
  • The skeleton takes the same column list, so loading and loaded tables always line up.
  • Pagination needs an exact total. With an estimate or uneven pages, use "Load more" instead.

Do and don't

Do

Give each column a width its content fits comfortably in (the relationship cell needs 300px).

Don't

Pick a small width and let the content spill into the next column.

Do

Share one column list between the table and its skeleton.

Don't

Write the skeleton's columns separately.

Do

Measure the loaded row and pass rowHeight (and pagination when there's a pager).

Don't

Use short stub rows: the table jumps when the data arrives. (This handbook's own example jumped 126px until it did.)

Do

Use the built-in spacing.

Don't

Pass padding classes to one table to make it look different.

Content

  • Column labels: one or two words, sentence case. An empty cell shows a muted dash (—), never blank.

Accessibility

  • Uses table roles (table, row, columnheader, cell), so screen readers can move by row and column. Name the table with aria-label.
  • Numbers that get compared use tabular-nums so they line up.
  • Header explanations use Tip and can be reached with Tab.
KeysAction
TabMoves to each clickable row, and to buttons inside rows.
EnterSpaceOpens the focused row.

API reference

DataTable

The table.

PropTypeDefaultDescription
columnsDataTableColumn<T>[]—{ key, label, icon, minWidth, weight?, tooltip?, headerContent?, render }.
rowsT[]—The rows to show (one page).
rowKey(row) => string—A stable id per row.
onRowClick(row) => void—Makes rows clickable.
onRowPrefetch(row) => void—Warm data on hover or focus.
rowProps(row) => attributes—Extra attributes for each row (id, aria-*, data-*). The table's own row props win.
aria-labelstring—Names the table.

DataTableSkeleton

The loading table.

PropTypeDefaultDescription
columnscolumn list without render—The same columns as the table.
rowsnumber5How many placeholder rows.
rowHeightnumber—A loaded row's height without its divider (56 for a row with a 28px avatar), so nothing jumps.
paginationbooleanfalseReserve the pager's 52px. New in the package.

DataTablePagination

The pager.

PropTypeDefaultDescription
pagenumber—Current page, from 1.
pageSizenumber—Rows per page.
totalCountnumber—Exact total rows.
loadingbooleanfalseSpinner on the current page; buttons disabled.
onPageChange(page) => void—Go to a page.

DataTableHeader

The default header (icon + label + optional tooltip), for building custom headers.

PropTypeDefaultDescription

Lineage

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

Used by components
None
Used in templates
Admin tableTable page

Known gaps

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

  • Bridge OS's clickable rows only respond to the mouse; the package's respond to the keyboard too.
  • Bridge OS's header explanations use the browser's title; the package uses Tip.
  • Bridge OS's current page fills with --primary, which fails contrast in dark mode; the package uses --primary-solid.
  • Bridge OS's admin apps-table and sharing-table aren't on DataTable (drag, editing, bulk select). They stay separate for now.

Sources

  • Bridge OS components.md → DataTable / DataTableSkeleton, DataTablePagination; src/components/ui/data-table.tsx, relationship-cell.tsx (ThIcon).