Data display

Data table

A table with sorting, search, pagination and row selection built in.

Client componentSource

Import

import { DataTable } from '@the_viveksingh/vivek-ui'

Sort, search, paginate, select

render and sortAccessor mean raw API rows go straight in — nested objects and nulls included — with no pre-transform.

Team members
Ada LovelaceEngineering2026-01-04
Alan Turing2026-02-11
Grace HopperCompilers2026-01-22
Katherine JohnsonFlight2026-03-02
interface Row { id: string; name: string; team: { name: string } | null; joined: string }

<DataTable
  data={users}
  rowKey="id"
  pageSize={5}
  searchable
  selectable
  columns={[
    { key: 'name', header: 'Name', sortable: true },
    {
      key: 'team',
      header: 'Team',
      render: (row: Row) => row.team?.name ?? '—',
      sortAccessor: (row: Row) => row.team?.name ?? '',
      sortable: true,
    },
    { key: 'joined', header: 'Joined', align: 'end', sortable: true },
  ]}
  caption="Team members"
/>

Props

Generated from the package's own type declarations, so this table cannot drift from the code.

Props for DataTable
PropTypeDefaultDescription
datareadonly Row[]
columnsreadonly Column<Row>[]
rowKey requiredExtract<keyof Row, string> | ((row: Row) => SelectionKey)Stable identity for a row: a property name, or a function returning one. Required, and deliberately without an index argument. Keying rows by array index is the bug that makes a selected row "move" when you sort — the checkbox stays on position 3 instead of on the row you ticked.
rowLabel(row: Row, index: number) => stringHuman name for a row, used for its selection checkbox. Defaults to the first column's value.
sizeTableSize
stripedboolean
borderedboolean
hoverableboolean
stickyHeaderboolean
responsive'scroll' | 'stack'`'scroll'` (default) keeps the grid and scrolls it sideways inside its own box. `'stack'` additionally collapses each row into a labelled card once the table's own container gets narrow — a container query, so it depends on the space the table has rather than on the size of the window.
captionReactNode`<caption>` content. The most robust way to name a table.
hideCaptionbooleanKeep the caption for screen readers only.
sortDataTableSort | nullControlled sort. `null` means unsorted; omit the prop entirely to stay uncontrolled.
defaultSortDataTableSort | null
onSortChange(sort: DataTableSort | null) => void
searchablebooleanRender the search field.
searchKeysreadonly ColumnKey<Row>[]Columns (or plain row properties) to search. Defaults to every visible column.
searchstring
defaultSearchstring
onSearchChange(search: string) => void
pageSizenumberRows per page. Omit for no pagination.
pagenumberControlled page, 1-based.
defaultPagenumber
onPageChange(page: number) => void
totalRowsnumberRow count for `manual` mode, where `data` holds only the current page.
manualbooleanTurn off local filtering, sorting and slicing. The escape hatch for server-driven tables: you get `onSortChange`, `onSearchChange` and `onPageChange`, you fetch, and you pass back the page you fetched plus `totalRows`. Without this the component would filter and slice your already-sliced page and show nothing.
selectablebooleanRender the selection column.
selectedKeysreadonly SelectionKey[]
defaultSelectedKeysreadonly SelectionKey[]
onSelectionChange(keys: SelectionKey[], rows: Row[]) => void
loadingboolean
skeletonRowsnumberSkeleton row count while `loading`. Defaults to `pageSize`, capped at 5.
emptyStateReactNodeShown instead of rows when there are none.
toolbarReactNodeExtra controls beside the search field — an export button, filters, a column picker.
rowHeaderColumnKey<Row>Column to render as `<th scope="row">`, naming its row for screen readers.
labelsPartial<DataTableLabels>

Every remaining prop is spread onto the root element, so all standard HTML and ARIA attributes work. className and style are merged with the library's own, never replaced, and the ref forwards to the root DOM node.

Rendering