OPERATOR

DataTable

Basic

Service health
SERVICELATENCYSTATUS
gateway42success
auth12warning
cache88error
queue7info

Sortable

Headers cycle ascending → descending → unsorted.

Service health
gateway42success
auth12warning
cache88error
queue7info

Externally sorted rows

Pass sortDescriptor when the caller sorts rows. It sets header aria-sort and a direction glyph without changing order or adding sort buttons, even with sortable.

<DataTable
  columns={columns}
  data={sortedRows}
  sortDescriptor={{ key: "latency", direction: "asc" }}
  caption="Service health"
/>

sortDescriptor={null} preserves externally managed source order. Omit it for internal sorting. With a descriptor, the caller owns controls and row ordering.

Dense

Dense rows have a 32px minimum height and 4px of vertical padding per cell. Default rows have a 40px minimum and 8px of vertical padding. Both grow to fit content, so badges and controls retain clearance from row borders. Each cell owns its 16px horizontal inset; text actions should avoid adding another inset.

Service health
SERVICELATENCYSTATUS
gateway42success
auth12warning
cache88error
queue7info

Cell rendering

asFormat
number, timestampRight-aligned mono with a matching right-aligned header; align overrides both
codeMono
badgeText-only labels: success, warning, error, info use semantic colors; other values use neutral text

Set highlight to accent a text column.

A number cell keeps its value on one line. With fixed column widths, a long number wraps only between its digit groups and keeps its unit with the last group: 1,234,567.890 kPa can wrap as 1,234, over 567.890 kPa, never between digits or before its unit. A number without group separators never wraps, unless it has more than 15 digits: that is an identifier, such as a serial number, and wraps like one. Timestamps wrap at their spaces.

Width and size

When the columns do not fit, the table scrolls sideways inside its own box and the page never widens. While it scrolls, the box is a keyboard tab stop named after the caption ("Service health scroll viewport"), so arrow keys can scroll it; while the table fits, it adds no tab stop.

A sortable table that is not windowed is designed for about 150 rows. Sorting moves every row and the browser lays them all out again, so a larger table responds slowly to a header click. In development, a sortable table with more rows and no virtualization logs a warning. Use DataGrid, which pages or windows its rows, for larger result sets.

Empty state

Headers remain visible. Set emptyMessage to explain the result; place recovery actions outside the table.

Filtered service health
SERVICELATENCYSTATUS
No services match your filters.

See Operations Map for search, filters, and row details.

Props

For user-controlled widths, pinned columns, saved preferences, and large result sets, use DataGrid. DataTable also accepts columnWidths, pinnedColumnKeys, and virtualization as low-level composition props. Put pinned columns first in descriptor order and provide numeric widths for their sticky offsets. Supply stable getRowId values and a separate accessible paginated alternative when using virtualization directly. Keep getRowId pure: the same row always gives the same ID. The table reads row IDs again only when data changes, not when only the function does. rowIndexOffset and totalRowCount describe a subset's position within the full result set; both exclude the header. Native ARIA row indices and counts include the header automatically.

Column render(value, originalRow) callbacks execute only for mounted rows. Raw row values remain available for sorting and row activation. onRowClick(row, index) receives the row and its index in the supplied data, also after a header sort.

PROPTYPEDEFAULTREQUIRED
columns

Column descriptors. Each item: { key: string (required), label: string (required), as?: 'text'|'number'|'code'|'timestamp'|'badge' (default 'text'), highlight?: boolean (default false), sortable?: boolean (default inherits table 'sortable'), width?: string (CSS width), align?: 'left'|'right' }. 'number'/'timestamp' cells render mono + right-aligned; with columnWidths a 'number' cell wraps only between digit groups and keeps a unit with its last group; 'badge' maps the value to a text-only Badge (success/warning/error/info use their semantic color, otherwise neutral). Use a custom render for outlined badges or an explicit indicator.

array—yes
data

Array of row objects (Record<string, unknown>) keyed by column.key.

array—yes
sortable

Enables column header sort buttons (cycles ascending → descending → unsorted) when sortDescriptor is omitted. Per-column 'sortable: false' opts a column out. Designed for about 150 rows without virtualization: development warns above that; use DataGrid or virtualization for larger result sets.

booleanfalseno
sortDescriptor

Read-only { key: string, direction: 'asc' | 'desc' } metadata for rows already sorted by the caller. The matching header exposes aria-sort and a direction glyph; data order is preserved and internal header sort controls are disabled even when sortable is true. Pass null for externally managed source order. Omit to retain existing internal sortable behavior.

node—no
dense

Compact rows: 32px minimum height with 4px vertical cell padding. Default rows use a 40px minimum and 8px padding. Rows grow with content; explicit virtualization.rowHeight overrides these dimensions.

booleanfalseno
onRowClick

(row, indexInSuppliedData) => void. The index is the row's index in data, also after a header sort. Adds a native action button to the first cell while preserving table row semantics. Text and number cells become the button label; React content remains independent beside an Open row button. Nested controls and editable content do not activate the row. Enter/Space activate the row button.

function—no
getRowId

(row, index) => string. Stable React key per row; falls back to the index. Keep it pure: IDs are read again only when data changes, not when only the function does.

function—no
caption

Accessible table caption, visually hidden (sr-only). It also names the table's scroll region, '<caption> scroll viewport': a table whose columns do not fit scrolls sideways inside its own box, which is a keyboard tab stop while it scrolls; the page never widens.

string—no
emptyMessage

Message spanning all columns when data is empty.

string"No results."no
columnWidths

Record<columnKey, CSS pixel width> for fixed table layout. Unspecified columns use 160 pixels. DataGrid provides bounded user controls.

node—no
pinnedColumnKeys

Column keys to keep sticky at the leading edge. Supply pinned columns first in columns, in the desired order, with columnWidths for accurate offsets. DataGrid manages this ordering automatically.

array—no
virtualization

Optional {height, rowHeight?, estimateRowHeight?, overscan?}. Native rows are windowed in a labeled scrolling region. Omit rowHeight for measured variable heights; fixed rows clip overlong content. Prefer DataGrid for its built-in paginated fallback. Stable getRowId is required for retained focus across data changes.

node—no
rowIndexOffset

Zero-based offset into the complete result set. Data rows announce offset + local index + 2, including the header.

number0no
totalRowCount

Total data rows excluding the header. Native table aria-rowcount includes the header as one additional row.

number—no

Accessibility

  • Native table with column headers and a visually hidden caption.
  • Columns that do not fit scroll inside a named region that takes keyboard focus; the page never scrolls sideways.
  • Sort buttons expose aria-sort and a direction chevron.
  • onRowClick adds a first-cell button supporting Enter/Space. Text/numbers name it; React content remains independent beside an “Open row” button.
  • Cell controls, labels, and editable content do not trigger row actions.
  • Dense rows retain 24px targets.

Manifest entry

<DataTable
  columns={[
    { key: "service", label: "SERVICE" },
    { key: "latency", label: "LATENCY", as: "number", highlight: true },
    { key: "status", label: "STATUS", as: "badge" }
  ]}
  data={rows}
  sortable
/>
View agent manifest
{
  "description": "Data-driven table with sorting, highlighting, and cell rendering.",
  "props": {
    "columns": {
      "type": "array",
      "items": {
        "key": "string (required)",
        "label": "string (required)",
        "as": "enum [text, number, code, timestamp, badge] (default: text)",
        "highlight": "boolean (default: false)",
        "sortable": "boolean (default: inherits from table)",
        "width": "string",
        "align": "enum [left, right]",
        "render": "(value, originalRow) => ReactNode; called for mounted cells only"
      },
      "required": true,
      "description": "Column descriptors. Each item: { key: string (required), label: string (required), as?: 'text'|'number'|'code'|'timestamp'|'badge' (default 'text'), highlight?: boolean (default false), sortable?: boolean (default inherits table 'sortable'), width?: string (CSS width), align?: 'left'|'right' }. 'number'/'timestamp' cells render mono + right-aligned; with columnWidths a 'number' cell wraps only between digit groups and keeps a unit with its last group; 'badge' maps the value to a text-only Badge (success/warning/error/info use their semantic color, otherwise neutral). Use a custom render for outlined badges or an explicit indicator."
    },
    "data": {
      "type": "array",
      "required": true,
      "description": "Array of row objects (Record<string, unknown>) keyed by column.key."
    },
    "sortable": {
      "type": "boolean",
      "default": false,
      "description": "Enables column header sort buttons (cycles ascending → descending → unsorted) when sortDescriptor is omitted. Per-column 'sortable: false' opts a column out. Designed for about 150 rows without virtualization: development warns above that; use DataGrid or virtualization for larger result sets."
    },
    "sortDescriptor": {
      "type": "node",
      "description": "Read-only { key: string, direction: 'asc' | 'desc' } metadata for rows already sorted by the caller. The matching header exposes aria-sort and a direction glyph; data order is preserved and internal header sort controls are disabled even when sortable is true. Pass null for externally managed source order. Omit to retain existing internal sortable behavior."
    },
    "dense": {
      "type": "boolean",
      "default": false,
      "description": "Compact rows: 32px minimum height with 4px vertical cell padding. Default rows use a 40px minimum and 8px padding. Rows grow with content; explicit virtualization.rowHeight overrides these dimensions."
    },
    "onRowClick": {
      "type": "function",
      "description": "(row, indexInSuppliedData) => void. The index is the row's index in data, also after a header sort. Adds a native action button to the first cell while preserving table row semantics. Text and number cells become the button label; React content remains independent beside an Open row button. Nested controls and editable content do not activate the row. Enter/Space activate the row button."
    },
    "getRowId": {
      "type": "function",
      "description": "(row, index) => string. Stable React key per row; falls back to the index. Keep it pure: IDs are read again only when data changes, not when only the function does."
    },
    "caption": {
      "type": "string",
      "description": "Accessible table caption, visually hidden (sr-only). It also names the table's scroll region, '<caption> scroll viewport': a table whose columns do not fit scrolls sideways inside its own box, which is a keyboard tab stop while it scrolls; the page never widens."
    },
    "emptyMessage": {
      "type": "string",
      "default": "No results.",
      "description": "Message spanning all columns when data is empty."
    },
    "columnWidths": {
      "type": "node",
      "description": "Record<columnKey, CSS pixel width> for fixed table layout. Unspecified columns use 160 pixels. DataGrid provides bounded user controls."
    },
    "pinnedColumnKeys": {
      "type": "array",
      "description": "Column keys to keep sticky at the leading edge. Supply pinned columns first in columns, in the desired order, with columnWidths for accurate offsets. DataGrid manages this ordering automatically."
    },
    "virtualization": {
      "type": "node",
      "description": "Optional {height, rowHeight?, estimateRowHeight?, overscan?}. Native rows are windowed in a labeled scrolling region. Omit rowHeight for measured variable heights; fixed rows clip overlong content. Prefer DataGrid for its built-in paginated fallback. Stable getRowId is required for retained focus across data changes."
    },
    "rowIndexOffset": {
      "type": "number",
      "default": 0,
      "description": "Zero-based offset into the complete result set. Data rows announce offset + local index + 2, including the header."
    },
    "totalRowCount": {
      "type": "number",
      "description": "Total data rows excluding the header. Native table aria-rowcount includes the header as one additional row."
    }
  },
  "composable_with": [
    "Card"
  ],
  "never_inside": [
    "DataTable"
  ],
  "example": "<DataTable\n  columns={[\n    { key: \"service\", label: \"SERVICE\" },\n    { key: \"latency\", label: \"LATENCY\", as: \"number\", highlight: true },\n    { key: \"status\", label: \"STATUS\", as: \"badge\" }\n  ]}\n  data={rows}\n  sortable\n/>"
}

Status cells

Columns with as="badge" use text-only status labels so repeated borders and indicators do not compete with table values. Semantic values retain their color, and the wording communicates their meaning. Use a custom render when a standalone outlined badge or an additional indicator is useful.

On this page