Usage
The data table has one core idea: options in, instance out.
const table = useDataTable(options) // enriched TanStack instance
return <DataTable table={table} /> // renders it
The hook
useDataTable(options) wraps TanStack's useTable. It:
- supplies default row models (core, sorted, filtered, faceted, grouped, expanded, paginated) — TanStack v9 no longer accepts row-model options, so the table wires them itself;
- injects display columns when needed (selection, row numbers, drag handle, expand, row actions);
- merges localization and icons (built-in defaults →
DataTableConfigProvider→ per-call options); - adds to the instance: UI state in
table.getState(), methods such astable.setDensity, and the resolved options ontable.dataTable.
Because it extends TanStack's TableOptions, anything TanStack accepts passes through: controlled state, on*Change handlers, manual* server-side flags, getRowId, getSubRows, initialState, and so on.
const table = useDataTable({
data,
columns,
getRowId: (row) => row.id,
// data-table options
enableGrouping: true,
// standard TanStack options, passed straight through
state: { sorting },
onSortingChange: setSorting,
manualPagination: true,
rowCount: total,
})
The component
<DataTable table={table} /> reads table.dataTable and renders the full layout: top toolbar → alert banner → drop-to-group zone → bordered scroll surface (sticky header, optional filter row, body, optional sticky footer) → pagination.
Any extra div props land on its root — see DataTable props. To style or add props to an inner part, such as bounding the scroll surface for sticky headers or virtualization, use slotProps:
const table = useDataTable({
data,
columns,
slotProps: { surface: { className: "max-h-[600px]" } },
})
The instance
The table's parts read their configuration from the instance rather than via props. You can use it too — for toolbar actions, custom layouts, or programmatic control:
renderTopToolbarCustomActions: ({ table }) => (
<Button onClick={() => table.beginCreate()}>New row</Button>
)
The UI state (density, full screen, filter visibility, editing…) is in table.getState(), its methods are on table (setDensity, beginRowEdit, cancelEdit, …), and every resolved option and DOM ref is on table.dataTable. See the full Table instance reference.
Columns
Columns are TanStack column defs, typed with DataTableColumnDef. The table reads extra per-column configuration from columnDef.meta — filter variant, alignment, edit variant, aggregation labels, click-to-copy, and more. See Column options.
const columns: DataTableColumnDef<Person>[] = [
{
accessorKey: "status",
header: "Status",
meta: {
filterVariant: "multi-select",
filterSelectOptions: STATUS_OPTIONS,
},
},
{
accessorKey: "salary",
header: "Salary",
aggregationFn: "sum",
meta: {
align: "right",
filterVariant: "range-slider",
editVariant: "number",
},
},
]
Column helper
createDataTableColumnHelper is TanStack's column helper with the table's features filled in. accessor infers each column's value type from a key or an accessor function, so getValue() in cell is typed without annotations, and meta takes the table's column options.
import { createDataTableColumnHelper } from "@/components/ui/data-table"
const helper = createDataTableColumnHelper<Person>()
const columns = helper.columns([
helper.accessor("name", { header: "Name" }),
helper.accessor((row) => row.salary / 12, {
id: "monthly",
header: "Monthly",
cell: ({ getValue }) => getValue().toFixed(0), // getValue(): number
meta: { align: "right" },
}),
helper.display({ id: "actions", cell: ({ row }) => <RowMenu row={row} /> }),
])
App-wide defaults
Wrap a subtree in DataTableConfigProvider to set default icons and localization for every table inside it, without editing each call site:
import { DataTableConfigProvider } from "@/components/ui/data-table"
;<DataTableConfigProvider localization={spanish} icons={lucideIcons}>
<App />
</DataTableConfigProvider>
Precedence is: built-in defaults → provider → per-call useDataTable({ localization, icons }).