DataTable
Keyboard-accessible data grid. Pass data rows with stable id values and columns built with createDataTableColumnHelper; enable selectable and pageSize as needed.
When to use
- Admin lists, reports and dense tabular data.
- Sortable / paginated / selectable rows of records.
- Numeric reports: right-align amount columns with columnAlign and pin the totals row with footer + stickyFooter.
Consider an alternative
- Work-item lists with inline property pickers — use ListView.
- Workflow columns — use KanbanBoard with KanbanColumn.
Import
import { DataTable } from '@revido-llc/kit';Keyboard interaction
- Arrow keys — move between cells
- Enter / Space — activate the focused cell control
- Tab — leave the grid for the next page control
Props
Props documented by the Kit. Refer to the TypeScript types for the complete interface, including inherited props.
Related exports
createDataTableColumnHelper
Examples
Literal rows and columns
JSON-safe accessor keys and scalar rows; use a helper for application renderers.
<DataTable aria-label="People" data={[{ id: "ada", name: "Ada", role: "Engineer" }, { id: "grace", name: "Grace", role: "Lead" }]} columns={[{ accessorKey: "name", header: "Name", enableSorting: true }, { accessorKey: "role", header: "Role" }]} />Sortable + paginated
Click a header to sort; page through rows.
const helper = createDataTableColumnHelper<Person>();
const columns = helper.columns([
helper.accessor("name", { header: "Name", enableSorting: true }),
helper.accessor("role", { header: "Role" }),
]);
<DataTable aria-label="Team" data={people} columns={columns} pageSize={20} />Selectable
<DataTable selectable rowSelection={selection} onRowSelectionChange={setSelection} … />Sticky totals, row presses and loading
Names, headers and the totals row stay visible while scrolling; the amount column is right-aligned. Row presses retain modifier keys and the row element. Loading and empty results retain headers.
<DataTable aria-label="Entries" data={entries} columns={columns} stickyHeader scrollClassName="max-h-80" stickyFirstColumn hidePagination
columnAlign={{ amount: "end" }} footer={[{ content: "All entries" }, { content: total }]} stickyFooter
onRowPress={(row, event) => openRecord(row.id, { trigger: event.currentTarget, newTab: event.metaKey || event.ctrlKey })} />
<DataTable aria-label="Team" data={[]} columns={columns} loading loadingRows={5} />
<DataTable aria-label="Team" data={[]} columns={columns} emptyState={<EmptyState icon={Users} title="No people found" />} />URL-controlled rows
Manual transforms preserve server results. Update sort/order/page/pageSize in one URL replacement, retaining unrelated parameters. Inspect opens full-width details; group labels and expansion state belong to the app.
// searchParams and replaceUrl come from the consuming app's router.
const sorting = [{ id: searchParams.get("sort") ?? "name", desc: searchParams.get("order") === "desc" }];
const pagination = { pageIndex: Number(searchParams.get("page") ?? 1) - 1, pageSize: Number(searchParams.get("pageSize") ?? 25) };
function updateUrl(patch) {
const next = new URLSearchParams(searchParams);
for (const [key, value] of Object.entries(patch)) next.set(key, String(value));
replaceUrl(next.toString());
}
<DataTable aria-label="People" data={serverRows} columns={columns} manualSorting manualPagination total={total}
sorting={sorting} onSortingChange={(next) => updateUrl({ sort: next[0]?.id ?? "name", order: next[0]?.desc ? "desc" : "asc", page: 1 })}
pagination={pagination} onPaginationChange={(next) => updateUrl({ page: next.pageIndex + 1, pageSize: next.pageSize })}
pageSizeOptions={[10, 25, 50]} onRowAction={(row) => openRecord(row.id)}
renderGroupHeader={(row, previous) => row.group !== previous?.group ? row.groupLabel : null}
renderRowDetails={(row) => expandedIds.has(row.id) ? <Details record={row} /> : null} />
// Domain metadata and totals: spans match the column count.
// rowDataAttributes={(row) => ({ "data-person": row.personId })}
// footer={[{ content: "All entries", colSpan: 2 }, { content: totals.amount }]}
// footerDataAttributes={{ "data-testid": "reconciliation-total" }}
// Independent row controls: helper.display({ id: "actions", header: "Actions", cell: ({ row }) => <Button onPress={() => inspect(row.original)}>Inspect</Button> })