| INV-1041 | Northwind Traders | paid | $1,420.00 | |
| INV-1042 | Contoso | pending | $380.00 | |
| INV-1043 | Fabrikam | overdue | $6,250.00 |
Installation
Nothing to add. This component ships as source in your download, so it is already in your project.
File
src/vui/data-table.tsxImport from
@viliha/vui-react/data-tableThe 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 whenvalueis 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
| Prop | Type | Default |
|---|---|---|
| direction | SortDirection | null | — |
DataTable props
| Prop | Type | Default | What it does |
|---|---|---|---|
| data | readonly T[] | — | |
| columns | readonly 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? | number | 0 | Rows 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 | — |