Table
Lists records that share the same fields.
Examples
Runs48
Worst first
48 rows, sorted by Status worst first
Recent runs3
| Run | Agent | Duration | Status |
|---|---|---|---|
| run_8f2a | support-agent | 0.9s | Failed |
| run_8f29 | triage | 41.0s | Slow |
| run_8f28 | refund-agent | 12.8s | Running |
Approvals5
3 waiting on you
5 rows, sorted by Status worst first
Tickets0Live
0 rows
8 rows
Runs8
8 rows, 3 selected
Sizes
Run Agent Duration Status run_8f2a support-agent 0.9s Failed run_8f29 triage 41.0s Slow sm
Run Agent Duration Status run_8f2a support-agent 0.9s Failed run_8f29 triage 41.0s Slow defaultdefault
Run Agent Duration Status run_8f2a support-agent 0.9s Failed run_8f29 triage 41.0s Slow lg
Kind
run_8f2a support-agent 0.9s Failed run_8f29 triage 41.0s Slow plaindefault
run_8f2a support-agent 0.9s Failed run_8f29 triage 41.0s Slow run
States
run_8f28 Running rest
run_8f28 Running 4m agohover
run_8f28 Running focus
run_8f28 Running selected
run_8f28 Running open
1 rows
disabled
Loading rows
loading
0 rows
empty
Usage
When to use it
- Use
- Many records with the same fields that a person compares across columns: runs, prompt releases, keys.
- Use
- A list the person sorts, filters or acts on in bulk.
- Use
- Exact values that need to line up: durations, counts, costs.
- Not for
- Each row is one line of mixed content and no columns compare. Use Item.
Rules
11 rules
- Must
- Give every table aria-label or a TableCaption. Because a screen reader announces an unnamed "table", and a page with two cannot be told apart in the tables list.
- Must
- Set meta.severity on a status column and open with defaultSorting={[{id: 'status', desc: true}]}. Because sorted by the alphabet, warn lands at the bottom below ok and idle, where an operator scanning for trouble misses it.
- Must
- Render a status cell with Status, which prints the word, not a colored dot or tint. Because hue alone vanishes in forced colors and grayscale, and a screen reader reads an empty cell.
- Must
- Set meta.unit to duration or count; never format each cell by hand. Because per-cell units put 950ms above 1.2s in one column, so the bigger-looking number is the smaller one.
- Must
- Open details with onRowClick and activeRowId; keep checkboxes for selection. The open row gets aria-current and the neutral grey, a selected row a ticked checkbox and the kind wash. Because opening by selection ticks a checkbox the person never pressed, so a bulk action then hits that row.
- Must
- Give a table that can pass 200 rows a maxHeight so it windows. Because without maxHeight DataTable renders every row, and 10,000 rows stall the page on each sort and filter.
- Never
- Wrap a Table in a Card or a second frame. Because the table is already one frame, and a second edge around it reads as two components.
- Should
- Pass title rather than writing a heading above the table.
- Should
- Say why in empty, top-left, with the query in a CodeTag, and name the way out; under a FilterBar its Clear all owns the reset.
- Never
- Stripe rows, add vertical cell borders, or color row text; the status word carries fail. Because stripes and red row text compete with the status word, the one place a failed run is meant to show.
- Should
- Fit seven columns in the 980px a list gets at 1280: size time with meta.time, give one name or ID column the rest with Truncate, and give every column but name, status and actions meta.hideBelow: 768.
Accessibility
8 checks
- The table is named by aria-label or caption and sets aria-rowcount and aria-busy while loading.
- Heads set aria-sort; Enter and Space on a sort button cycle it and Shift adds the column; a status line announces the row count, selection and sort politely.
- Rows set aria-selected, aria-current on the open row, and aria-rowindex.
- Checkboxes are named Select row n and Select all rows, with mixed for partial; Shift-click applies the anchor row state to the range.
- With onRowClick the body is one tab stop; arrows, Home, End, Page Up and Page Down move; Enter opens once.
- A resize handle is role="separator"; Left and Right move 16px, Shift 64px, Enter resets; arrows swap in RTL.
- Forced colors give selected and open rows a Highlight outline.
- BulkActionBar is a toolbar with one tab stop and arrow keys between actions; Escape clears the selection, and a polite status says "3 runs selected" and "Selection cleared".
Edge cases
8 edge cases
- 120-character agent, 36-character run ID
- Cells never wrap; they cut with an ellipsis and keep full text in the DOM.
- data=[]
- TableEmpty top-left in the visible width.
- 10,000 rows
- With maxHeight 360 about 20 rows stay in the DOM; the sort runs over all.
- Narrow pane
- hideBelow columns drop first; then the box scrolls sideways, joins the tab order so arrows scroll it, and pins the actions column to the end with a row rule on its edge.
- Right to left
- Padding, numeric alignment and the resize handle mirror.
- Loading
- The header stays; after --dur-skeleton-delay placeholder bars fill the real columns and stay at least --dur-skeleton-hold. A reload keeps the rows until then.
- Button or checkbox inside a row
- Row clicks ignore them, so an action never also opens the row.
- Warn, fail and waiting rows
- A warn row takes no wash and a fail row a faint 40% one; the status word carries the hue. A waiting status is the plain ink word in a DataTable row; the header holds the one InkSlot count.
API
24 props
columnsColumnDef<TData, any>[]defaultrequireddataTData[]defaultrequiredaria-labelstringdefaultrequiredgetRowId(row, index) => stringdefaultindexdefaultSorting, sorting, onSortingChangeSortingStatedefault[]rowSelection, onRowSelectionChange, enableRowSelectionRowSelectionState, boolean | (row) => booleandefault{}, truecolumnVisibility, defaultColumnVisibilityVisibilityStatedefault{}enableColumnResizingbooleandefaulttrueonRowClick, activeRowId(row) => void, stringrowActions, rowIndicator, rowActionsWidth(row) => ReactNodedefault72TableRowActionsMenu label, TableRowActionsItem variant, TableRowActionsSeparatorstring, 'default' | 'destructive'defaultrequired, 'default'loading, loadingRowsboolean, numberdefaultfalse, 6emptyReactNodedefault'No rows.'title, actions, footerReactNodelive, follow, rowStatusboolean, boolean, (row) => warn | faildefaultfalsemaxHeight, virtualizeFromnumber | string, numberdefault200size, kind'sm' | 'default', Kinddefault'default'toolbar(table) => ReactNodeBulkActionBar count, noun, onClearnumber, [string, string], () => voiddefaultrequired, ['item', 'items'], -BulkActionBar position, BulkAction status'inline' | 'floating', 'fail'default'inline'Table title, count, actions, toolbar, footer, status, liveReactNode, warn | fail, booleanmeta.severitybooleandefaultfalsemeta.unit'duration' | 'count'meta.numeric, meta.mono, meta.hideBelow, meta.timeboolean, boolean, number, boolean
Tokens
23 tokens
--line-frame- oklch(var(--ink-c) / var(--line-frame-alpha))
--line-row- oklch(var(--ink-c) / 0.08)
--surface-page- var(--paper)
--surface-hover- oklch(var(--ink-c) / 0.05)
--accent-wash- var(--surface-selected)
--accent-mark- var(--ink)
--ink- oklch(var(--ink-c))
--row-sm- 32px
--row- 40px
--row-lg- 56px
--status-column- 128px
--time-column- 12ch
--radius-soft- 6px
--radius-inner- 4px
--radius-frame- 10px
--control-sm- 28px
--dur-fast- 120ms
--ease-step- cubic-bezier(0.2, 0, 0, 1)
--surface-skeleton- color-mix(in oklch, oklch(var(--grey-c)), oklch(var(--ink-c)) var(--skeleton-step))
--type-label-sm- 400 12px/16px var(--font-sans)
--type-tag- var(--type-code-sm)
--text-3- oklch(var(--ink-c) / 0.66)
--elev-overlay- 0 0 0 1px var(--line-frame), 0 12px 24px -12px oklch(var(--text-on-field-c) / var(--shadow-alpha))