DataTable
A semantic table for lists of servers, tools and calls: sticky header, row links, selection, row menus, and empty and loading states.
Examples
Rows with links and actions
rowHref stretches a link in the first cell over the row. The trailing cell is usually a row Menu.
const columns: DataTableColumn<Server>[] = [
{ key: 'name', header: 'Name', width: '40%' },
{ key: 'status', header: 'Status', cell: (s) => <Lamp state={s.state} label={s.status} showLabel /> },
{ key: 'transport', header: 'Transport', hideBelow: 'sm', cell: (s) => <Badge mono>{s.transport}</Badge> },
{ key: 'tools', header: 'Tools', numeric: true },
]
<Card>
<CardBody flush>
<DataTable
caption="Servers"
columns={columns}
rows={servers}
rowKey={(s) => s.id}
rowHref={(s) => `/servers/${s.id}`}
actions={(s) => <Menu label={`Actions for ${s.name}`} items={rowActions(s)} />}
/>
</CardBody>
</Card>Selectable and compact
Native checkboxes named by selectable.name post the row keys with the surrounding form. The header checkbox selects all.
| Name | Status | Transport | Tools | |
|---|---|---|---|---|
| GitHub | Connected | HTTP | 26 | |
| Linear | Connected | HTTP | 12 | |
| Sentry | Token expires soon | SSE | 18 | |
| Notion | Failed | HTTP | 0 |
<DataTable
caption="Servers to pause"
density="compact"
selectable={{ name: 'server' }}
columns={columns}
rows={servers}
rowKey={(s) => s.id}
rowLabel={(s) => s.name}
/>Loading and empty
Both states render inside the real table, so the header and columns do not jump.
| Name | Status | Transport | Tools |
|---|---|---|---|
| Name | Status | Transport | Tools |
|---|---|---|---|
No servers yet Add an MCP server to route its tools through Switchboard. | |||
<DataTable caption="Servers" loading loadingRows={3} columns={columns} rows={[]} rowKey={(s) => s.id} />
<DataTable
caption="Servers"
columns={columns}
rows={[]}
rowKey={(s) => s.id}
empty={<EmptyState size="sm" icon="server" title="No servers yet" body="Add an MCP server to route its tools through Switchboard." />}
/>Usage
import { DataTable } from '@switchboard/ui'
<Card>
<CardBody flush>
<DataTable caption="Tools" columns={columns} rows={tools} rowKey={(t) => t.name} />
</CardBody>
</Card>Props
Every component also takes mix, for css() and mixins applied after its own style.
DataTable
| Prop | Type | Default | Description |
|---|---|---|---|
| caption* | string | – | Table caption and the name of the scroll region. Visually hidden unless captionVisible. |
| columns* | DataTableColumn<Row>[] | – | Column definitions, see below. |
| rows* | Row[] | – | The data. |
| rowKey* | (row: Row) => string | – | Stable key per row; also the value a selection checkbox posts. |
| rowHref | (row: Row) => string | undefined | – | Whole-row link: an <a> in the first cell stretched over the row. |
| rowLabel | (row: Row) => string | – | Short row name for the selection checkbox ("Select GitHub"). Defaults to the key. |
| selectable | { name: string } | – | Adds a checkbox column; checkboxes are named `name` and post with a surrounding form. |
| actions | (row: Row) => RemixNode | – | Trailing cell, usually a row Menu. |
| loading | boolean | – | Render skeleton rows instead of data and set aria-busy. |
| loadingRows | number | 4 | Number of skeleton rows. |
| empty | RemixNode | – | Shown in a full-width cell when there are no rows. Usually an EmptyState. |
| density | 'default' | 'compact' | 'default' | Row height and text size. |
| captionVisible | boolean | – | Show the caption above the table. |
| maxHeight | string | – | Scroll vertically inside the wrapper so the sticky header has room to stick. |
Also accepts every <div> attribute; they go on the scrolling wrapper.
DataTableColumn
| Prop | Type | Default | Description |
|---|---|---|---|
| key* | string | – | Column key; the default cell reads row[key]. |
| header* | RemixNode | – | Header cell content. |
| cell | (row: Row) => RemixNode | – | Cell content. Defaults to String(row[key]). |
| numeric | boolean | – | Right-aligned, tabular figures. For counts, durations, money. |
| align | 'start' | 'center' | 'end' | – | Text alignment. |
| width | string | – | CSS width for the <col>, e.g. "40%" or "120px". |
| hideBelow | 'sm' | 'md' | – | Hide the column under 560px (sm) or 768px (md). |
| truncate | boolean | – | Flexible text column: takes the remaining width and ends long text with an ellipsis instead of widening the table. |
| hideHeader | boolean | – | Header is read by assistive tech but not shown (icon columns). |
Accessibility
- A real <table> with a <caption>. The first cell of each row is a <th scope="row">, so screen readers announce the row name with each cell.
- The wrapper is a focusable role="region" named by the caption, so a wide table can be scrolled with the keyboard.
- Row links are ordinary <a> elements in the first cell; checkboxes and menus sit above the stretched link and stay clickable.
- Selection checkboxes are named "Select {rowLabel}"; the header checkbox shows a mixed state when some rows are selected.
Guidance
Do
- Put the table in a Card with CardBody flush.
- Mark count, duration and money columns numeric.
- Hide secondary columns on phones with hideBelow rather than letting the table scroll.
Don’t
- Put more than one link or button in the first cell of a linked row.
- Build a table from divs and grids; use DataTable so the semantics come for free.