Skip to content

Documentation

neelam-ui

Search documentation

Getting Started
Forms
Overlays
Navigation
Data Display
Layout
AI & Chat
Blocks
GitHub repository

Data Table

A sortable, optionally filterable and paginated table driven by a columns/data pair. Built on Table, Pagination, and Input.

Email
Ada Lovelaceada@example.comEngineer98
Grace Hoppergrace@example.comEngineer95
Alan Turingalan@example.comResearcher99
Katherine Johnsonkatherine@example.comMathematician97
Margaret Hamiltonmargaret@example.comEngineer96

Usage#

import { DataTable, type DataTableColumn } from "neelam-ui";
 
interface Person {
  id: number;
  name: string;
  role: string;
  score: number;
}
 
const columns: DataTableColumn<Person>[] = [
  { key: "name", header: "Name", sortable: true },
  { key: "role", header: "Role", sortable: true },
  { key: "score", header: "Score", sortable: true, align: "right" },
];
 
<DataTable columns={columns} data={people} getRowId={(row) => row.id} />

Order of operations#

Filtering runs before sorting and pagination. That ordering is what makes the page count and sort order describe the rows actually on screen, rather than the unfiltered set — a common off-by-a-page bug when the three are composed in the wrong order.

Columns#

key must be a key of your row type, so a typo is a type error rather than a column of undefined.

Custom cells#

cell takes over rendering for a column:

{
  key: "role",
  header: "Role",
  sortable: true,
  cell: (row) => <Badge variant="secondary">{row.role}</Badge>,
  filterValue: (row) => row.role,
}

Custom cells need filterValue

Once cell returns an element rather than a string, the filter has no text to match against and that column silently stops being searchable. filterValue gives it the raw string back. The same applies to sortValue for columns whose displayed value is not itself sortable — a formatted date, for example.

Sorting a formatted value#

{
  key: "createdAt",
  header: "Created",
  sortable: true,
  cell: (row) => formatDate(row.createdAt),
  sortValue: (row) => row.createdAt.getTime(),
}

Without sortValue, "1 April" sorts before "1 March" — alphabetically correct and chronologically wrong.

Filtering#

filterable adds a search box above the table that matches across every column. Match counts are announced through a live region, so a screen reader user learns that the result set changed without having to go looking.

<DataTable
  columns={columns}
  data={people}
  filterable
  filterLabel="Filter people"
  noMatchesMessage="No people match that search."
/>

Pagination#

Set pageSize to paginate; omit it to render every row. To place the pagination controls somewhere else — outside a card the table sits inside, say — combine hidePagination with onPaginationChange and render your own, while DataTable keeps owning the sort, filter, and page maths:

const [pagination, setPagination] = useState(null);
 
<DataTable
  columns={columns}
  data={people}
  pageSize={10}
  hidePagination
  onPaginationChange={setPagination}
/>;
 
{pagination && (
  <Pagination
    page={pagination.page}
    totalPages={pagination.totalPages}
    onPageChange={pagination.setPage}
  />
)}

hidePagination has no effect without pageSize.

Keyboard#

Keyboard shortcuts
KeyBehaviour
TabReaches the filter box, each sortable column header, and the pagination controls in visual order.
EnterSpaceOn a sortable column header, cycles ascending → descending → unsorted.

Accessibility#

  • Renders a real <table> with <th scope="col"> headers, so table navigation commands work in screen readers.
  • Sortable headers are <button>s carrying aria-sort, which is updated as the sort cycles — the current sort is announced, not merely drawn as an arrow.
  • Filter match counts are announced through a live region.
  • The empty state distinguishes "no data at all" (emptyMessage) from "your filter excluded everything" (noMatchesMessage), which are very different situations for the user.

Deliberate omissions#

Row selection, column resizing, column reordering, virtualisation, and server-side data are not included. Each would push the component from a presentation concern into a state-management one, and the reasoning is recorded in DECISIONS.md in the repository. For those cases, compose Table with your own logic.

API reference#

DataTable#

Props for DataTable
PropTypeDefault
columnsrequiredDataTableColumn<T>[]
datarequiredT[]
emptyMessageReactNodeNo results.
filterable

Shows a search box above the table that filters rows across every column.

booleanfalse
filterLabel

The search box's accessible name. Defaults to `"Filter rows"`.

stringFilter rows
filterPlaceholderstringSearch…
getRowId

Identifies each row for React's `key` — defaults to its index, which is fine unless rows are reordered by sorting across a paginated boundary in a way that would matter for e.g. focus/animation state, which this component doesn't have anyway.

((row: T, index: number) => string | number)
hidePagination

Suppresses the built-in pagination footer while `pageSize` still pages the rows internally exactly as before — pair with `onPaginationChange` to render an equivalent footer somewhere else, e.g. outside a card DataTable itself renders inside. Has no effect without `pageSize`.

booleanfalse
noMatchesMessage

Shown in place of `emptyMessage` when a filter is what emptied the table.

ReactNodeNo rows match your filter.
onPaginationChange

Reports the current page, total page count, and a setter, whenever any of them changes — a page turn, or the row count changing under a filter or a new `data` prop. DataTable still owns the state either way; this just also hands it outward, which only matters paired with `hidePagination`.

((state: DataTablePaginationState) => void)
pageSize

Rows per page. Omit to disable pagination and render every row.

number

DataTableColumn#

Props for DataTableColumn
PropTypeDefault
headerrequiredReactNode
keyrequired

Must be a key of `T` — read as the cell's value unless `cell` is given, and used as this column's React key.

keyof T & string
alignAlign
cell

Custom cell content. Defaults to `String(row[key])`.

((row: T) => ReactNode)
classNamestring
filterValue

The text this column contributes to the filter. Defaults to `String(row[key])` — set it for columns whose `cell` renders something the raw value doesn't describe (an avatar, a status badge), or pass `() => ""` to exclude the column from filtering entirely.

((row: T) => string)
sortable

Enables click-to-sort on this column's header.

boolean
sortValue

Custom sort key, for columns whose displayed value isn't itself sortable (e.g. a formatted date). Defaults to `row[key]`.

((row: T) => string | number)