Data Table
Powerful data tables built on the Table primitives and TanStack Table — sorting, filtering, pagination, column visibility, row selection, and interactive rows.
| Status | Tax docs | ||||
|---|---|---|---|---|---|
WWWhitfield Wealth | £110,522.53 | 11 Jun | Pending details | ||
HPHartley Planning | £980.00 | 11 Jun | Pending details | ||
MRMercer & Rowe | £213,456.78 | 10 Jun | |||
CFCalder Financial Planning | £99,999.99 | 10 Jun | Pending details | ||
AAAshdown Advisory | £1,205,329.91 | 9 Jun | |||
FWFenwick Wealth | £75,018.01 | 8 Jun | Pending details | ||
TAThorne Associates | £1,293.35 | 7 Jun | |||
BWBeaumont Wealth | £120,348.21 | 7 Jun | |||
KPKingsley Planning | £8,767.77 | 6 Jun | |||
RIRavenscroft IFA | £42,189.40 | 6 Jun |
const table = useDataTable({ columns, data, pagination: true, initialState: { sorting: [{ id: "lastPaid", desc: true }] },});const docsFilter = (table.getColumn("taxDocs")?.getFilterValue() as string) ?? "all";const setDocsFilter = (value: string) => table.getColumn("taxDocs")?.setFilterValue(value === "all" ? undefined : value);return ( <VStack gap={4} className="w-full"> <HStack className="w-full items-center justify-between"> <Heading as="div" size="2xl">Recipients</Heading> <Button size="sm"> <Plus data-icon="inline-start" /> Create recipient </Button> </HStack> <HStack className="w-full items-center" gap={1}> <ToggleGroup type="single" variant="outline" size="sm" value={docsFilter} onValueChange={(value) => value && setDocsFilter(value)} > {docsFilters.map(([value, label]) => ( <ToggleGroupItem key={value} value={value}> {label} </ToggleGroupItem> ))} </ToggleGroup> <HStack className="ml-auto items-center" gap={2}> <Input size="sm" placeholder="Filter recipients..." value={(table.getColumn("name")?.getFilterValue() as string) ?? ""} onChange={(event) => table.getColumn("name")?.setFilterValue(event.target.value)} className="w-56" /> <DataTableViewOptions table={table} /> </HStack> </HStack> <DataTable size="lg" table={table} onRowClick={(row) => row.toggleSelected()} /> <DataTablePagination table={table} /> </VStack>);DataTable follows the shadcn/ui Data Table guide: it is a thin rendering layer over TanStack Table (headless sorting, filtering, pagination, selection — you keep full control via column definitions and the table instance) and the Table primitives (Reva styling). Column APIs, row models, and state management are TanStack's — anything in their docs works here.
Usage
import {
DataTable,
DataTableColumnHeader,
DataTableHoverActionsCell,
DataTablePagination,
DataTableViewOptions,
useDataTable,
} from "@reva/ui";
import type { ColumnDef } from "@tanstack/react-table";Define the shape of your data and your columns:
type Payment = {
id: string;
amount: number;
status: "pending" | "processing" | "success" | "failed";
email: string;
};
const columns: ColumnDef<Payment>[] = [
{ accessorKey: "status", header: "Status" },
{ accessorKey: "email", header: "Email" },
{ accessorKey: "amount", header: "Amount" },
];Then either pass columns and data directly for the zero-config path:
<DataTable columns={columns} data={data} />…or create the table instance with useDataTable (which wires sorting, filtering, column visibility, and row selection state for you) when you need to compose a toolbar, filter inputs, or pagination around the table:
const table = useDataTable({ columns, data, pagination: true });
return (
<VStack gap={4}>
<DataTable table={table} />
<DataTablePagination table={table} />
</VStack>
);For full control (server-side pagination, lifted state, custom row models) drop down to TanStack's useReactTable and pass the instance to DataTable the same way.
Give columns and data stable identities
Define columns and data at module scope, in state, or behind useMemo — never inline expressions like rows.filter(...) or rows.slice(0, 5) in the component body. TanStack recomputes its row models whenever either reference changes, and on paginated tables the page auto-reset then queues a state update on every recompute: with a value that's re-created each render this loops until the tab hangs. useDataTable disables the auto-reset for non-paginated tables, but paginated ones still need stable references.
Examples
Basic table
The minimal case: column definitions plus row data. Cells render however the column definition says — here the status column renders a StatusBadge and the amount column formats currency and right-aligns it (TanStack's cell formatting pattern: a header/cell render function on the column).
const columns: ColumnDef<Payment>[] = [
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => <StatusBadge intent={statusVariant[row.original.status]}>{row.original.status}</StatusBadge>,
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "amount",
header: () => <div className="text-right">Amount</div>,
cell: ({ row }) => {
const formatted = new Intl.NumberFormat("en-GB", {
style: "currency",
currency: "GBP",
}).format(row.original.amount);
return <div className="text-right font-medium tabular-nums">{formatted}</div>;
},
},
];| Status | Amount | |
|---|---|---|
| success | anna@whitfieldwealth.co.uk | £316.00 |
| success | james@hartleyplanning.co.uk | £242.00 |
| processing | sophie@mercerrowe.co.uk | £837.00 |
| success | oliver@calderfp.co.uk | £874.00 |
| failed | claire@ashdownadvisory.co.uk | £721.00 |
<DataTable columns={columns} data={data} />Row actions
Add an actions column whose cell returns a DropdownMenu. Access the row data with row.original to handle actions — for example using the id for a DELETE call to your API. Opening the menu doesn't highlight a row here — the highlight is reserved for interactive rows, where an open menu holds it instead; the expanded trigger button carries its own state. Row actions work inside interactive rows too: clicks on the menu never trigger row navigation.
| Status | Amount | ||
|---|---|---|---|
| success | anna@whitfieldwealth.co.uk | £316.00 | |
| success | james@hartleyplanning.co.uk | £242.00 | |
| processing | sophie@mercerrowe.co.uk | £837.00 | |
| success | oliver@calderfp.co.uk | £874.00 | |
| failed | claire@ashdownadvisory.co.uk | £721.00 |
const columns: ColumnDef<Payment>[] = [ // ... { id: "actions", cell: ({ row }) => { const payment = row.original; return ( <DropdownMenu> <DropdownMenuTrigger asChild> <IconButton variant="ghost" size="sm"> <span className="sr-only">Open menu</span> <DotsThree /> </IconButton> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuLabel>Actions</DropdownMenuLabel> <DropdownMenuItem onClick={() => navigator.clipboard.writeText(payment.id)}> Copy payment ID </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem>View client</DropdownMenuItem> <DropdownMenuItem>View payment details</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ); }, },];Pagination
Set pagination to paginate rows client-side (pages of 10 by default — control the page size via TanStack's initialState). Wire controls with the table.previousPage() / table.nextPage() API, or use the ready-made DataTablePagination below.
| Status | Amount | |
|---|---|---|
| success | anna@whitfieldwealth.co.uk | £316.00 |
| success | james@hartleyplanning.co.uk | £242.00 |
| processing | sophie@mercerrowe.co.uk | £837.00 |
| success | oliver@calderfp.co.uk | £874.00 |
| failed | claire@ashdownadvisory.co.uk | £721.00 |
const table = useDataTable({ columns, data, pagination: true, initialState: { pagination: { pageSize: 5 } },});return ( <VStack gap={4} className="w-full"> <DataTable table={table} /> <HStack className="w-full justify-end" gap={2}> <Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()} > Previous </Button> <Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()} > Next </Button> </HStack> </VStack>);Sorting
Sorting state is wired automatically — make a header sortable by rendering a toggle in the column's header function. Click the status, email, or amount header to cycle ascending → descending → unsorted: the arrow appears only while a direction is active and shows which one. The toggle is a ghost Button with px-2.5, which would offset the label from the body cells, so a left-aligned header pairs it with -ml-2.5 to pull the label back flush with the column below — the offset is most obvious on the first column. For right-aligned (numeric) columns, wrap the toggle in a text-right container, move the negative margin to the trailing edge (-mr-2.5), and lead with the icon — supporting icons sit on the open side, away from the alignment edge. The dropdown version (asc / desc / clear / hide), DataTableColumnHeader, bakes this alignment in and reveals its double-arrow menu affordance on hover, showing the active direction once sorted, handling alignment via its align prop. When a sortable header sits in the first or last column, pass bleed — the toggle's hover fill extends past the content edge and would otherwise be clipped at the table box.
| success | anna@whitfieldwealth.co.uk | £316.00 |
| success | james@hartleyplanning.co.uk | £242.00 |
| processing | sophie@mercerrowe.co.uk | £837.00 |
| success | oliver@calderfp.co.uk | £874.00 |
| failed | claire@ashdownadvisory.co.uk | £721.00 |
const columns: ColumnDef<Payment>[] = [ { accessorKey: "status", // First column: -ml-2.5 cancels the ghost Button's px-2.5 so the label // stays flush with the body cells below. header: ({ column }) => ( <Button variant="ghost" size="sm" className="-ml-2.5 h-8 gap-1.5 px-2.5 text-xs font-medium text-fg-muted hover:text-fg-default [&_svg:not([class*='size-'])]:size-3.5" onClick={column.getToggleSortingHandler()} > Status {column.getIsSorted() === "desc" ? ( <ArrowDown /> ) : column.getIsSorted() === "asc" ? ( <ArrowUp /> ) : null} </Button> ), cell: ({ row }) => <StatusCell status={row.original.status} />, }, { accessorKey: "email", header: ({ column }) => ( <Button variant="ghost" size="sm" className="-ml-2.5 h-8 gap-1.5 px-2.5 text-xs font-medium text-fg-muted hover:text-fg-default [&_svg:not([class*='size-'])]:size-3.5" onClick={column.getToggleSortingHandler()} > Email {column.getIsSorted() === "desc" ? ( <ArrowDown /> ) : column.getIsSorted() === "asc" ? ( <ArrowUp /> ) : null} </Button> ), }, { accessorKey: "amount", header: ({ column }) => ( <div className="text-right"> <Button variant="ghost" size="sm" className="-mr-2.5 h-8 gap-1.5 px-2.5 text-xs font-medium text-fg-muted hover:text-fg-default [&_svg:not([class*='size-'])]:size-3.5" onClick={column.getToggleSortingHandler()} > {column.getIsSorted() === "desc" ? ( <ArrowDown /> ) : column.getIsSorted() === "asc" ? ( <ArrowUp /> ) : null} Amount </Button> </div> ), },];Filtering
Filter state is wired automatically — bind an Input to a column's filter value via table.getColumn(id). Filtering composes with pagination: the page count follows the filtered set.
| Status | Amount | |
|---|---|---|
| success | anna@whitfieldwealth.co.uk | £316.00 |
| success | james@hartleyplanning.co.uk | £242.00 |
| processing | sophie@mercerrowe.co.uk | £837.00 |
| success | oliver@calderfp.co.uk | £874.00 |
| failed | claire@ashdownadvisory.co.uk | £721.00 |
const table = useDataTable({ columns, data, pagination: true });return ( <VStack gap={4} className="w-full"> <Input size="sm" placeholder="Filter emails..." value={(table.getColumn("email")?.getFilterValue() as string) ?? ""} onChange={(event) => table.getColumn("email")?.setFilterValue(event.target.value)} className="max-w-sm" /> <DataTable table={table} /> </VStack>);Visibility
Column visibility state is wired automatically — toggle columns through the table instance's column API. Calling preventDefault in the checkbox item's onSelect keeps the menu open across toggles, so several columns can be changed in one visit; clicking outside or on the trigger closes it. The ready-made version is DataTableViewOptions, which behaves the same way.
| Status | Amount | |
|---|---|---|
| success | anna@whitfieldwealth.co.uk | £316.00 |
| success | james@hartleyplanning.co.uk | £242.00 |
| processing | sophie@mercerrowe.co.uk | £837.00 |
| success | oliver@calderfp.co.uk | £874.00 |
| failed | claire@ashdownadvisory.co.uk | £721.00 |
const table = useDataTable({ columns, data });return ( <VStack gap={4} className="w-full"> <HStack className="w-full justify-end"> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="outline" size="sm"> Columns <CaretDown data-icon="inline-end" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> {table .getAllColumns() .filter((column) => column.getCanHide()) .map((column) => ( <DropdownMenuCheckboxItem key={column.id} className="capitalize" checked={column.getIsVisible()} onCheckedChange={(value) => column.toggleVisibility(!!value)} onSelect={(event) => event.preventDefault()} > {column.id} </DropdownMenuCheckboxItem> ))} </DropdownMenuContent> </DropdownMenu> </HStack> <DataTable table={table} /> </VStack>);Row selection
Add a select column rendering a Checkbox in the header (select all on page) and each cell (select row). The checkbox column is automatically inset from the table edge, and selected rows deliberately get no row highlight — the checked checkbox carries the state. Read the selection with table.getFilteredSelectedRowModel().
const columns: ColumnDef<Payment>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
// ...
];| Status | Amount | ||
|---|---|---|---|
| success | anna@whitfieldwealth.co.uk | £316.00 | |
| success | james@hartleyplanning.co.uk | £242.00 | |
| processing | sophie@mercerrowe.co.uk | £837.00 | |
| success | oliver@calderfp.co.uk | £874.00 | |
| failed | claire@ashdownadvisory.co.uk | £721.00 |
0 of 5 row(s) selected.
const table = useDataTable({ columns, data });return ( <VStack gap={4} className="w-full"> <DataTable table={table} /> <Text size="sm" color="muted-foreground"> {table.getFilteredSelectedRowModel().rows.length} of{" "} {table.getFilteredRowModel().rows.length} row(s) selected. </Text> </VStack>);Sizes
The size prop controls density — row height and cell padding. The default is deliberately spacious (48px rows); reach for sm or xs only when an information-dense screen genuinely needs them, and lg for hero/marketing surfaces.
xs — 32px rows
| Status | Amount | |
|---|---|---|
| success | anna@whitfieldwealth.co.uk | £316.00 |
| success | james@hartleyplanning.co.uk | £242.00 |
| processing | sophie@mercerrowe.co.uk | £837.00 |
sm — 40px rows
| Status | Amount | |
|---|---|---|
| success | anna@whitfieldwealth.co.uk | £316.00 |
| success | james@hartleyplanning.co.uk | £242.00 |
| processing | sophie@mercerrowe.co.uk | £837.00 |
default — 48px rows — spacious by default
| Status | Amount | |
|---|---|---|
| success | anna@whitfieldwealth.co.uk | £316.00 |
| success | james@hartleyplanning.co.uk | £242.00 |
| processing | sophie@mercerrowe.co.uk | £837.00 |
lg — 64px rows
| Status | Amount | |
|---|---|---|
| success | anna@whitfieldwealth.co.uk | £316.00 |
| success | james@hartleyplanning.co.uk | £242.00 |
| processing | sophie@mercerrowe.co.uk | £837.00 |
<DataTable size="xs" columns={columns} data={data} /><DataTable size="sm" columns={columns} data={data} /><DataTable columns={columns} data={data} /><DataTable size="lg" columns={columns} data={data} />Interactive rows
Pass onRowClick to make rows interactive — the primary row pattern: hover for a rounded card-like highlight with a subtle outline, click to navigate to a detail screen. The highlight lifts the row off the list (the adjacent hairlines fade out under it), stays put while the row's actions menu is open — the user is still on that row — and interactive standalone tables bleed automatically: the first and last columns stay flush with the surrounding layout while the highlight overflows them on each side. Rows become keyboard-focusable and activate with Enter or Space. Clicks on interactive descendants (checkboxes, row-action menus, links, buttons) are ignored, so row actions and selection keep working inside interactive rows.
| Status | Amount | ||
|---|---|---|---|
| success | anna@whitfieldwealth.co.uk | £316.00 | |
| success | james@hartleyplanning.co.uk | £242.00 | |
| processing | sophie@mercerrowe.co.uk | £837.00 | |
| success | oliver@calderfp.co.uk | £874.00 | |
| failed | claire@ashdownadvisory.co.uk | £721.00 |
Click a row to open its detail screen.
<DataTable columns={columns} data={data} onRowClick={(row) => navigate(`/payments/${row.original.id}`)}/>Hover actions
Use DataTableHoverActionsCell for a trailing column that shows quiet metadata at rest and swaps to action controls on hover or keyboard focus — the payments-inbox pattern. Pass children for the rest-state content and actions for what appears on hover. Give the column a fixed width matching its header div so columns don't shift.
{
id: "lastUpdated",
header: () => <div className="w-[150px] text-right">Last updated</div>,
cell: ({ row }) => (
<DataTableHoverActionsCell
actions={<>
<IconButton variant="destructive" size="sm" className="rounded-full">
<span className="sr-only">Delete bill</span>
<Trash />
</IconButton>
<Button size="sm">
Review
<CaretRight data-icon="inline-end" />
</Button>
</>}
>
{row.original.lastUpdated}
</DataTableHoverActionsCell>
),
}| Recipient | Due date | Status | Amount | Invoice no. | Last updated |
|---|---|---|---|---|---|
WWWhitfield Wealth | Apr 2026 | overdue | £220.00 | INV-902 | |
HPHartley Planning | Dec 2026 | scheduled | £1,290.00 | INV-001 | |
HHMRC | 17 Jan | overdue | £11,600.00 | INV-883346 | |
MRMercer & Rowe | 3 Feb | paid | £540.50 | INV-1204 | |
CFCalder Financial Planning | 28 Feb | scheduled | £275.25 | INV-1188 |
Hover a row to reveal its actions; click anywhere else on the row to open it.
<DataTable size="lg" columns={billColumns} data={bills} onRowClick={(row) => openBill(row.original)}/>Under the hood
Every body row carries a group/row class. DataTableHoverActionsCell uses group-hover/row:, group-focus-visible/row:, and group-has-[:focus-visible]/row: — covering pointer hover, tabbing to the row, and focus moving onto the revealed controls. group-focus-within/row: is excluded: it also matches plain click-focus, which would pin the actions open after a row click. To build a fully custom layout, use these three variants directly on your own show/hide elements.
Within Card
For a Card-embedded data table use the bleed pattern from the Table page: <CardContent className="px-0!"> plus variant="embedded" and an inset matching the Card size, so the subtle header band spans edge-to-edge and the first column aligns with the CardHeader title. In this variant the row highlight runs edge-to-edge too — square corners, meeting the Card edges instead of floating as a lifted card — and the Card's inset gives sortable headers their hover-fill room, no bleed needed.
| Status | ||
|---|---|---|
| success | anna@whitfieldwealth.co.uk | £316.00 |
| success | james@hartleyplanning.co.uk | £242.00 |
| processing | sophie@mercerrowe.co.uk | £837.00 |
| success | oliver@calderfp.co.uk | £874.00 |
| failed | claire@ashdownadvisory.co.uk | £721.00 |
const table = useDataTable({ columns, data });return ( <Card> <CardHeader> <CardTitle>Recent payments</CardTitle> <CardAction> <DataTableViewOptions table={table} /> </CardAction> </CardHeader> <CardContent className="px-0!"> <DataTable variant="embedded" inset="default" table={table} onRowClick={(row) => navigate(`/payments/${row.original.id}`)} /> </CardContent> </Card>);Pinned columns
Freeze columns to an edge while the rest scroll horizontally — for wide grids like a per-period breakdown. Pass columnPinning with the column ids to freeze ({ left: ["metric"] }, or right); the DataTable reads TanStack's pinning state and applies the sticky offset, a frosted-glass backdrop (a strong backdrop-blur over the transparent cell — surface-agnostic, no fill to match), and a divider against the scrolling centre. Pinning a single column is always exact; pinning several needs explicit column sizes so the offsets line up. With an external table instance, set the pinning on it instead of passing the prop. Pinned tables scroll inside the branded ScrollArea (hover-revealed horizontal bar) rather than the native scrollbar — pass scrollArea={false} to opt back out.
<DataTable columns={columns} data={data} columnPinning={{ left: ["metric"] }}/>Reusable components
Three ready-made building blocks compose around a shared table instance.
Column header — makes any column sortable and hideable via a dropdown (asc / desc / clear sort / hide). Unsorted headers read as plain labels: the double-arrow affordance fades in on hover, keyboard focus, or while the menu is open, and the active direction arrow stays visible once sorted:
const columns: ColumnDef<Payment>[] = [
{
accessorKey: "email",
header: ({ column }) => <DataTableColumnHeader column={column} title="Email" />,
},
];Pagination — rows-per-page select, page position, and first / previous / next / last controls (the boundary buttons appear from the lg breakpoint up). Set showSelectionCount={false} for tables without row selection:
<DataTablePagination table={table} />Column toggle — a View dropdown listing hideable columns (hidden below the lg breakpoint):
<DataTableViewOptions table={table} />| success | anna@whitfieldwealth.co.uk | £316.00 |
| success | james@hartleyplanning.co.uk | £242.00 |
| processing | sophie@mercerrowe.co.uk | £837.00 |
| success | oliver@calderfp.co.uk | £874.00 |
| failed | claire@ashdownadvisory.co.uk | £721.00 |
const table = useDataTable({ columns, data, pagination: true, initialState: { pagination: { pageSize: 5 } },});return ( <VStack gap={4} className="w-full"> <HStack className="w-full items-center" gap={2}> <Input size="sm" placeholder="Filter emails..." value={(table.getColumn("email")?.getFilterValue() as string) ?? ""} onChange={(event) => table.getColumn("email")?.setFilterValue(event.target.value)} className="max-w-sm" /> <DataTableViewOptions table={table} /> </HStack> <DataTable table={table} bleed /> <DataTablePagination table={table} pageSizeOptions={[5, 10, 20, 30]} showSelectionCount={false} /> </VStack>);Accessibility
- Interactive rows get
tabIndex={0}and activate with Enter or Space; keyboard focus shows the same rounded highlight as hover. - Hover-revealed actions must pair
group-focus-visible/row:andgroup-has-[:focus-visible]/row:withgroup-hover/row:so they appear for keyboard focus — on the row and on the revealed controls — without sticking after pointer clicks. - Selection checkboxes and row-action triggers carry
aria-label/sr-onlylabels in the examples above — keep them when adapting. - Headers rendered through
DataTableColumnHeaderexpose sorting as a regular button + menu, so sort controls are reachable and announced.
Props
DataTable
| Prop | Type | Default | Description |
|---|---|---|---|
columns | ColumnDef<TData, TValue>[] | — | Column definitions (TanStack). Ignored when table is passed. |
data | TData[] | — | Row data. Ignored when table is passed. |
table | Table<TData> (TanStack instance) | — | External instance from useDataTable / useReactTable; pass it to compose toolbars and pagination around the table. |
size | "xs" | "sm" | "default" | "lg" | "default" | Density step — 32px / 40px / 48px / 64px row height with matching cell padding. Body text is 12px at xs and 14px from sm up. The header band keeps a constant 40px at every step — the --table-header-height variable on Table, overridable via className. |
variant | "default" | "embedded" | "default" | Forwarded to Table. "default" — standalone page-level look; "embedded" — subtle header band for Card/Item-embedded tables. |
inset | "none" | "xs" | "sm" | "default" | "lg" | per variant; "sm" when bleeding | Forwarded to Table — horizontal padding on first/last column cells. bleed supplies its own ("sm") unless overridden. |
bleed | boolean | true when interactive and variant="default" | Gives the table box a 16px overhang each side so interactive fills can overflow the content: columns, hairlines, and the header underline all stay flush with the surrounding layout while row highlights and edge-column header hover fills paint past them. Pass it on standalone tables with sortable first/last-column headers; pass bleed={false} to opt an interactive table out. |
pagination | boolean | false | Paginate rows client-side. Only applies to the internally-created instance (when table is not passed). |
columnPinning | { left?: string[]; right?: string[] } | — | Freeze columns to the left / right edge by id while the rest scroll horizontally; the DataTable renders the sticky offset, a frosted-glass backdrop-blur panel, and a divider against the scrolling centre. Seeds the internal table's pinning — with an external table, set it there instead. |
scrollArea | boolean | true when any column is pinned | Scroll the table inside the Reva ScrollArea (branded, hover-revealed horizontal bar) instead of the native overflow-x-auto container. Reads live pinning state, so externally-controlled table instances get it too. |
onRowClick | (row: Row<TData>) => void | — | Makes rows interactive: pointer cursor, rounded hover/focus highlight (held while the row's menu is open), keyboard activation. Clicks on interactive descendants are ignored. |
emptyMessage | ReactNode | "No results." | Rendered in a single centred cell when there are no rows. |
useDataTable
Takes columns, data, optional pagination: boolean, plus any useReactTable option (merged on top — e.g. initialState, getRowId, an onSortingChange/state.sorting pair to lift sorting out). Wires internal state for sorting, column filters, column visibility, and row selection, and returns the TanStack Table instance.
DataTableColumnHeader
| Prop | Type | Default | Description |
|---|---|---|---|
column | Column<TData, TValue> | — | The column, from the header render context. |
title | string | — | Header label. Renders plain when the column has sorting disabled. |
align | "left" | "right" | "left" | Match the column's text alignment. "right" right-aligns the trigger, keeps the label flush with the values, and flips the icon to the leading side. |
DataTablePagination
| Prop | Type | Default | Description |
|---|---|---|---|
table | Table<TData> | — | The table instance. |
pageSizeOptions | readonly number[] | [10, 20, 30, 40, 50] | Options for the rows-per-page select. |
showSelectionCount | boolean | true | Show the "x of y row(s) selected." summary. |
DataTableViewOptions
| Prop | Type | Default | Description |
|---|---|---|---|
table | Table<TData> | — | The table instance. |
DataTableHoverActionsCell
| Prop | Type | Default | Description |
|---|---|---|---|
actions | ReactNode | — | Required. Controls shown on hover / keyboard focus. No opinion on content — IconButtons, a Button, a mix. |
children | ReactNode | — | Quiet metadata shown at rest; hidden when the row is active. Omit for an actions-only column. |
width | number | string | 150 | Width of the cell container. A number is treated as px; a string is used as-is. Must match the column header width. |
align | "start" | "end" | "end" | Flex alignment. "end" for trailing columns; "start" for leading. |
className | string | — | Extra classes on the container div. |