Content
- Column labels: one or two words, sentence case. An empty cell shows a muted dash (—), never blank.
The one table for product data, with its loading skeleton and pager.
import { … } from "@brdgapp/ui/components/data-table"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.
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.
Header row. Light tint, a divider below. Each header is an icon + 12px Bold label, or your own (ColumnSortButton, ColumnFilterMenu).
Rows. Dividers between rows; 24px padding at the outer edges, 12px between columns.
Cells. Whatever render returns; long text should truncate inside it.
Pagination. Optional, under the table: range, previous, page numbers, next.
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.
Click a row, or Tab to it and press Enter.
| Rest | Hover | Focus | |
|---|---|---|---|
| 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 |
| State | What happens |
|---|---|
| Loading | DataTableSkeleton with the same columns, inside DataState. |
| Empty / error | DataState's empty and error bodies. |
| Row hover | Faint tint; a clickable row gets a stronger tint and a pointer. |
| Row focus | Clickable rows: the same tint and a blue inner ring. |
weight) and never shrink below minWidth; below the total, the table scrolls sideways inside its card.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.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 passrowHeight (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.aria-label.tabular-nums so they line up.| Keys | Action |
|---|---|
| Tab | Moves to each clickable row, and to buttons inside rows. |
| EnterSpace | Opens the focused row. |
DataTable
The table.
| Prop | Type | Default | Description |
|---|---|---|---|
columns | DataTableColumn<T>[] | — | { key, label, icon, minWidth, weight?, tooltip?, headerContent?, render }. |
rows | T[] | — | 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-label | string | — | Names the table. |
DataTableSkeleton
The loading table.
| Prop | Type | Default | Description |
|---|---|---|---|
columns | column list without render | — | The same columns as the table. |
rows | number | 5 | How many placeholder rows. |
rowHeight | number | — | A loaded row's height without its divider (56 for a row with a 28px avatar), so nothing jumps. |
pagination | boolean | false | Reserve the pager's 52px. New in the package. |
DataTablePagination
The pager.
| Prop | Type | Default | Description |
|---|---|---|---|
page | number | — | Current page, from 1. |
pageSize | number | — | Rows per page. |
totalCount | number | — | Exact total rows. |
loading | boolean | false | Spinner 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.
| Prop | Type | Default | Description |
|---|
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.
title; the package uses Tip.--primary, which fails contrast in dark mode; the package uses --primary-solid.apps-table and sharing-table aren't on DataTable (drag, editing, bulk select). They stay separate for now.components.md → DataTable / DataTableSkeleton, DataTablePagination; src/components/ui/data-table.tsx, relationship-cell.tsx (ThIcon).