Data Table

Data Table sorts, searches, paginates and selects rows of an array you already have, entirely in the browser. For data that lives on a server, see RecordView.

INV-1041Northwind Traderspaid$1,420.00
INV-1042Contosopending$380.00
INV-1043Fabrikamoverdue$6,250.00

Installation

Nothing to add. This component ships as source in your download, so it is already in your project.

Filesrc/vui/data-table.tsx
Import from@viliha/vui-react/data-table

The exported names are at the top of that file. The import path is an alias the download's tsconfig.json points at src/vui/, so you can move that folder and change one line rather than every file that imports from it.

Usage

Hand it an array and a list of columns. Sorting, searching, pagination, row selection and column visibility all run in the browser, so there is nothing to wire up and no request to make.

invoices-table.tsx
import { DataTable, type DataColumn } from "@viliha/vui-react/data-table";

type Invoice = { id: string; customer: string; amount: number };

const columns: DataColumn<Invoice>[] = [
  { key: "id", header: "Invoice", sortable: true },
  { key: "customer", header: "Customer", sortable: true },
  { key: "amount", header: "Amount", sortable: true, align: "end",
    cell: (row) => row.amount.toFixed(2) },
];

export function Example({ invoices }: { invoices: Invoice[] }) {
  return (
    <DataTable
      data={invoices}
      columns={columns}
      rowId={(row) => row.id}
      search
      pageSize={10}
      selectable
      columnToggle
    />
  );
}

Sorting and searching read the value, not the cell

cell is what renders; value is what the column sorts and searches on. Without the split, a column whose cell is a badge would sort by its markup, which is the bug every hand-rolled table ships once.

a status column
{
  key: "status",
  header: "Status",
  // What it sorts and searches on.
  value: (row) => row.status,
  // What you see.
  cell: (row) => <Badge>{row.status}</Badge>,
}

Column options

  • key — the column id, and the property read from the row when value is absent.
  • header, cell, value, sortable, align, className, hidden.

💡 When you want RecordView instead

DataTable works on data you already hold. If your rows live on a server and you need paging and filtering to happen there, plus typed per-field filters, bulk actions, an inline create and edit form and CSV, JSON and Excel import and export, that is RecordView, which is a Pro component. The split is explained at free and Pro.

No table engine

There is no @tanstack/react-table underneath, and no new dependency of any kind: the whole thing is four pure functions over the Tableprimitives this package already ships. The column API is shaped like TanStack's on purpose, so it reads the way you expect.

SortMark props

PropTypeDefault
directionSortDirection | null—

DataTable props

PropTypeDefaultWhat it does
datareadonly T[]—
columnsreadonly DataColumn<T>[]—
rowId?(row: T, index: number) => string—A stable identity per row, used for selection. Defaults to the array index, which is correct for data that does not reorder underneath the user and wrong for data that does.
caption?string—
search?boolean | { placeholder?: string }—One box searching every visible column. `true` takes the default placeholder.
pageSize?number0Rows per page. Omit, or pass 0, for no pagination.
selectable?boolean—A checkbox column, with select-all across the current page.
onSelectionChange?(rows: T[]) => void—
columnToggle?boolean—The show/hide menu.
defaultSort?DataSort—
loading?boolean—
empty?React.ReactNode"No data."What an empty table says. A search that matches nothing says so instead.
onRowClick?(row: T) => void—
toolbar?React.ReactNode—Extra controls, to the right of the toolbar.
className?string—