Data Table

Powerful data tables built on the Table primitives and TanStack Table — sorting, filtering, pagination, column visibility, row selection, and interactive rows.

Recipients
StatusTax docs
WWWhitfield Wealth
£110,522.53
11 JunPending details
HPHartley Planning
£980.00
11 JunPending details
MRMercer & Rowe
£213,456.78
10 Jun
CFCalder Financial Planning
£99,999.99
10 JunPending details
AAAshdown Advisory
£1,205,329.91
9 Jun
FWFenwick Wealth
£75,018.01
8 JunPending 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
0 of 13 row(s) selected.
Page 1 of 2
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>;
    },
  },
];
StatusEmail
Amount
successanna@whitfieldwealth.co.uk
£316.00
successjames@hartleyplanning.co.uk
£242.00
processingsophie@mercerrowe.co.uk
£837.00
successoliver@calderfp.co.uk
£874.00
failedclaire@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.

StatusEmail
Amount
successanna@whitfieldwealth.co.uk
£316.00
successjames@hartleyplanning.co.uk
£242.00
processingsophie@mercerrowe.co.uk
£837.00
successoliver@calderfp.co.uk
£874.00
failedclaire@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.

StatusEmail
Amount
successanna@whitfieldwealth.co.uk
£316.00
successjames@hartleyplanning.co.uk
£242.00
processingsophie@mercerrowe.co.uk
£837.00
successoliver@calderfp.co.uk
£874.00
failedclaire@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.

successanna@whitfieldwealth.co.uk
£316.00
successjames@hartleyplanning.co.uk
£242.00
processingsophie@mercerrowe.co.uk
£837.00
successoliver@calderfp.co.uk
£874.00
failedclaire@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.

StatusEmail
Amount
successanna@whitfieldwealth.co.uk
£316.00
successjames@hartleyplanning.co.uk
£242.00
processingsophie@mercerrowe.co.uk
£837.00
successoliver@calderfp.co.uk
£874.00
failedclaire@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.

StatusEmail
Amount
successanna@whitfieldwealth.co.uk
£316.00
successjames@hartleyplanning.co.uk
£242.00
processingsophie@mercerrowe.co.uk
£837.00
successoliver@calderfp.co.uk
£874.00
failedclaire@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,
  },
  // ...
];
StatusEmail
Amount
successanna@whitfieldwealth.co.uk
£316.00
successjames@hartleyplanning.co.uk
£242.00
processingsophie@mercerrowe.co.uk
£837.00
successoliver@calderfp.co.uk
£874.00
failedclaire@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

StatusEmail
Amount
successanna@whitfieldwealth.co.uk
£316.00
successjames@hartleyplanning.co.uk
£242.00
processingsophie@mercerrowe.co.uk
£837.00

sm 40px rows

StatusEmail
Amount
successanna@whitfieldwealth.co.uk
£316.00
successjames@hartleyplanning.co.uk
£242.00
processingsophie@mercerrowe.co.uk
£837.00

default 48px rows — spacious by default

StatusEmail
Amount
successanna@whitfieldwealth.co.uk
£316.00
successjames@hartleyplanning.co.uk
£242.00
processingsophie@mercerrowe.co.uk
£837.00

lg 64px rows

StatusEmail
Amount
successanna@whitfieldwealth.co.uk
£316.00
successjames@hartleyplanning.co.uk
£242.00
processingsophie@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.

StatusEmail
Amount
successanna@whitfieldwealth.co.uk
£316.00
successjames@hartleyplanning.co.uk
£242.00
processingsophie@mercerrowe.co.uk
£837.00
successoliver@calderfp.co.uk
£874.00
failedclaire@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>
  ),
}
RecipientDue dateStatus
Amount
Invoice no.
Last updated
WWWhitfield Wealth
Apr 2026overdue
£220.00
INV-902
10 Jun
HPHartley Planning
Dec 2026scheduled
£1,290.00
INV-001
10 Jun
HHMRC
17 Janoverdue
£11,600.00
INV-883346
10 Jun
MRMercer & Rowe
3 Febpaid
£540.50
INV-1204
9 Jun
CFCalder Financial Planning
28 Febscheduled
£275.25
INV-1188
8 Jun

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.

Recent payments
Status
successanna@whitfieldwealth.co.uk
£316.00
successjames@hartleyplanning.co.uk
£242.00
processingsophie@mercerrowe.co.uk
£837.00
successoliver@calderfp.co.uk
£874.00
failedclaire@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.

Movement
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
Money in
£150k
£155k
£159k
£164k
£169k
£174k
£179k
£184k
£190k
£196k
Money out
-£92k
-£94k
-£96k
-£98k
-£100k
-£102k
-£104k
-£106k
-£108k
-£110k
Net cash flow
£58k
£61k
£63k
£66k
£69k
£72k
£76k
£79k
£82k
£86k
<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} />
successanna@whitfieldwealth.co.uk
£316.00
successjames@hartleyplanning.co.uk
£242.00
processingsophie@mercerrowe.co.uk
£837.00
successoliver@calderfp.co.uk
£874.00
failedclaire@ashdownadvisory.co.uk
£721.00
Page 1 of 5
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: and group-has-[:focus-visible]/row: with group-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-only labels in the examples above — keep them when adapting.
  • Headers rendered through DataTableColumnHeader expose sorting as a regular button + menu, so sort controls are reachable and announced.

Props

DataTable

PropTypeDefaultDescription
columnsColumnDef<TData, TValue>[]Column definitions (TanStack). Ignored when table is passed.
dataTData[]Row data. Ignored when table is passed.
tableTable<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 bleedingForwarded to Table — horizontal padding on first/last column cells. bleed supplies its own ("sm") unless overridden.
bleedbooleantrue 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.
paginationbooleanfalsePaginate 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.
scrollAreabooleantrue when any column is pinnedScroll 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>) => voidMakes rows interactive: pointer cursor, rounded hover/focus highlight (held while the row's menu is open), keyboard activation. Clicks on interactive descendants are ignored.
emptyMessageReactNode"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

PropTypeDefaultDescription
columnColumn<TData, TValue>The column, from the header render context.
titlestringHeader 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

PropTypeDefaultDescription
tableTable<TData>The table instance.
pageSizeOptionsreadonly number[][10, 20, 30, 40, 50]Options for the rows-per-page select.
showSelectionCountbooleantrueShow the "x of y row(s) selected." summary.

DataTableViewOptions

PropTypeDefaultDescription
tableTable<TData>The table instance.

DataTableHoverActionsCell

PropTypeDefaultDescription
actionsReactNodeRequired. Controls shown on hover / keyboard focus. No opinion on content — IconButtons, a Button, a mix.
childrenReactNodeQuiet metadata shown at rest; hidden when the row is active. Omit for an actions-only column.
widthnumber | string150Width 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.
classNamestringExtra classes on the container div.

On this page