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.
| Ada Lovelace | Engineering | 2026-01-04 | |
| Alan Turing | — | 2026-02-11 | |
| Grace Hopper | Compilers | 2026-01-22 | |
| Katherine Johnson | Flight | 2026-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.
| Prop | Type | Default | Description |
|---|---|---|---|
data | readonly Row[] | — | — |
columns | readonly Column<Row>[] | — | — |
rowKey required | Extract<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) => string | — | Human name for a row, used for its selection checkbox. Defaults to the first column's value. |
size | TableSize | — | — |
striped | boolean | — | — |
bordered | boolean | — | — |
hoverable | boolean | — | — |
stickyHeader | boolean | — | — |
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. |
caption | ReactNode | — | `<caption>` content. The most robust way to name a table. |
hideCaption | boolean | — | Keep the caption for screen readers only. |
sort | DataTableSort | null | — | Controlled sort. `null` means unsorted; omit the prop entirely to stay uncontrolled. |
defaultSort | DataTableSort | null | — | — |
onSortChange | (sort: DataTableSort | null) => void | — | — |
searchable | boolean | — | Render the search field. |
searchKeys | readonly ColumnKey<Row>[] | — | Columns (or plain row properties) to search. Defaults to every visible column. |
search | string | — | — |
defaultSearch | string | — | — |
onSearchChange | (search: string) => void | — | — |
pageSize | number | — | Rows per page. Omit for no pagination. |
page | number | — | Controlled page, 1-based. |
defaultPage | number | — | — |
onPageChange | (page: number) => void | — | — |
totalRows | number | — | Row count for `manual` mode, where `data` holds only the current page. |
manual | boolean | — | Turn 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. |
selectable | boolean | — | Render the selection column. |
selectedKeys | readonly SelectionKey[] | — | — |
defaultSelectedKeys | readonly SelectionKey[] | — | — |
onSelectionChange | (keys: SelectionKey[], rows: Row[]) => void | — | — |
loading | boolean | — | — |
skeletonRows | number | — | Skeleton row count while `loading`. Defaults to `pageSize`, capped at 5. |
emptyState | ReactNode | — | Shown instead of rows when there are none. |
toolbar | ReactNode | — | Extra controls beside the search field — an export button, filters, a column picker. |
rowHeader | ColumnKey<Row> | — | Column to render as `<th scope="row">`, naming its row for screen readers. |
labels | Partial<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
Client component
DataTable declares 'use client' because it needs state, effects or event handlers. Importing it into a Server Component creates a client boundary at this component — everything above it stays on the server.