Skip to content

DataTable

A semantic table for lists of servers, tools and calls: sticky header, row links, selection, row menus, and empty and loading states.

DataServer-rendered

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.

Servers
NameStatusTransportToolsActions
GitHubConnectedHTTP26
LinearConnectedHTTP12
SentryToken expires soonSSE18
NotionFailedHTTP0
Example
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.

Servers to pause
NameStatusTransportTools
GitHubConnectedHTTP26
LinearConnectedHTTP12
SentryToken expires soonSSE18
NotionFailedHTTP0
Example
<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.

Servers (loading)
NameStatusTransportTools
Servers (empty)
NameStatusTransportTools

No servers yet

Add an MCP server to route its tools through Switchboard.

Example
<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

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

DataTable props
PropTypeDefaultDescription
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.
loadingboolean–Render skeleton rows instead of data and set aria-busy.
loadingRowsnumber4Number of skeleton rows.
emptyRemixNode–Shown in a full-width cell when there are no rows. Usually an EmptyState.
density'default' | 'compact''default'Row height and text size.
captionVisibleboolean–Show the caption above the table.
maxHeightstring–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

DataTableColumn props
PropTypeDefaultDescription
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]).
numericboolean–Right-aligned, tabular figures. For counts, durations, money.
align'start' | 'center' | 'end'–Text alignment.
widthstring–CSS width for the <col>, e.g. "40%" or "120px".
hideBelow'sm' | 'md'–Hide the column under 560px (sm) or 768px (md).
truncateboolean–Flexible text column: takes the remaining width and ends long text with an ellipsis instead of widening the table.
hideHeaderboolean–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.