Data Table

A Table driven by TanStack Table, with sorting, selection, pagination, column visibility and TanStack Query states.

View as Markdown
Status
successken99@acme.com$316.00
successabe45@acme.com$242.00
processingmonserrat44@acme.com$837.00
successsilas22@acme.com$874.00
failedcarmella@acme.com$721.00
pendingdee@acme.com$129.00
const features = tableFeatures({  rowSelectionFeature,  rowSortingFeature,  sortedRowModel: createSortedRowModel(),  sortFns: { alphanumeric: sortFn_alphanumeric, basic: sortFn_basic },});
const column = createColumnHelper<typeof features, Payment>();
const columns = column.columns([  column.display({    id: "select",    meta: { skeleton: "checkbox" },    header: ({ table }) => <DataTableSelectAll table={table} />,    cell: ({ row }) => <DataTableSelectRow row={row} />,    enableSorting: false,  }),  column.accessor("email", {    header: ({ column }) => (      <DataTableColumnHeader column={column} title="Email" />    ),    sortFn: "alphanumeric",  }),]);
const table = useTable({ columns, data, features });
<DataTable empty="No payments." label="Payments" table={table} />;

Installation

$ npx shadcn@latest add @constella/data-table

Usage

import { DataTable } from "@/components/tanstack/data-table";

Build the table with TanStack Table's useTable, with the features you need, and pass it to DataTable. It renders the headers and rows as a Table, and only the rows on screen, against the window's scroll.

When the table sits in an element that scrolls on its own, like an app's main area beside a sidebar, pass that element as scrollRef, as the Row Click example does with a ScrollArea's viewportRef.

The table's layout is fixed, as only the rows on screen render, so set narrow columns' widths with columnSx. Align a column of figures to its end with meta: { align: "end" } on the column: its header and cells follow, and a DataTableColumnHeader puts its sort icon first, so the label ends over the digits. Text too long for its column ends in an ellipsis. Give the table a minWidth in sx. On a narrow screen it then scrolls sideways, and a column without a width keeps its room.

Keep features and columns outside the component. A new array each render rebuilds every row model.

Composition

DataTable├── DataTableColumnHeader├── DataTableSelectAll├── DataTableSelectRow├── DataTablePagination└── DataTableViewOptions

Data Table Column Header

Use DataTableColumnHeader in a column's header to sort by it, ascending, descending, then off. Register rowSortingFeature and a sortedRowModel.

header: ({ column }) => (  <DataTableColumnHeader column={column} title="Amount" />);

Data Table Select All

Use DataTableSelectAll in a select column's header and DataTableSelectRow in its cells. Register rowSelectionFeature. A selected row takes the fill of a selected table row. meta: { skeleton: "checkbox" } makes the column load as a checkbox.

column.display({  id: "select",  meta: { skeleton: "checkbox" },  header: ({ table }) => <DataTableSelectAll table={table} />,  cell: ({ row }) => <DataTableSelectRow row={row} />,});

Filtering

Add globalFilteringFeature for a search across columns and columnFilteringFeature for one column's filter, with a filteredRowModel, and pass both through state. When nothing is left, empty fills the table. Put a row's actions in a last display column, here a Dropdown Menu. Add columnVisibilityFeature and DataTableViewOptions to let people hide columns; it lists those with data, named by labels.

Status
successken1@acme.com$969.00
pendingabe2@acme.com$116.00
failedmonserrat3@acme.com$253.00
processingsilas4@acme.com$390.00
successcarmella5@acme.com$527.00
pendingdee6@acme.com$664.00
failedana7@acme.com$801.00
processingben8@acme.com$938.00
successcy9@acme.com$85.00
pendingjo10@acme.com$222.00
failedken11@acme.com$359.00
processingabe12@acme.com$496.00
successmonserrat13@acme.com$633.00
pendingsilas14@acme.com$770.00
failedcarmella15@acme.com$907.00
processingdee16@acme.com$54.00
successana17@acme.com$191.00
pendingben18@acme.com$328.00
failedcy19@acme.com$465.00
processingjo20@acme.com$602.00
successken21@acme.com$739.00
pendingabe22@acme.com$876.00
failedmonserrat23@acme.com$23.00
processingsilas24@acme.com$160.00
successcarmella25@acme.com$297.00
pendingdee26@acme.com$434.00
failedana27@acme.com$571.00
processingben28@acme.com$708.00
successcy29@acme.com$845.00
pendingjo30@acme.com$982.00

0 of 36 selected

const features = tableFeatures({  columnFilteringFeature,  globalFilteringFeature,  rowSelectionFeature,  rowSortingFeature,  filteredRowModel: createFilteredRowModel(),  sortedRowModel: createSortedRowModel(),  filterFns: {    equalsString: filterFn_equalsString,    includesString: filterFn_includesString,  },});
const table = useTable({  columns,  data,  features,  globalFilterFn: "includesString",  onRowSelectionChange: setRowSelection,  state: { columnFilters, globalFilter, rowSelection },});

Row Click

onRowClick gets a click anywhere on a row except its controls, so a checkbox or menu in the row keeps its own click. Open the row from there, here in a Side Panel, and pass its id as activeRowId to mark it. Enter on a focused row opens it the same way.

With Cmd, Ctrl or Shift, a click on a row selects it instead, Shift through to the last one. Dragging from a row's checkbox across others gives them all its new state; dragging back puts them back.

<DataTable  activeRowId={openId}  onRowClick={(row) => {    setOpenId(row.id);  }}  table={table}  {...props}/>

Keyboard

When rows can be selected or clicked, one row takes Tab and the arrow keys move between them.

KeyDoes
↑ ↓The previous or next row
Home, EndThe first or last row
X, SpaceSelects the row, or clears it
Shift ↑ ↓Extends the selection; back over it shrinks it
EnterOpens the row through onRowClick

Query

Pass the useQuery result as query. While it's pending the body holds skeleton rows. With placeholderData: keepPreviousData, the old rows stay on screen and fade while the next ones load. If it fails with no row left, error takes the place of empty. The same table runs client-side, as here, or server-side with manualSorting, manualPagination and rowCount, with the state in the queryKey.

Status
const query = useQuery({  placeholderData: keepPreviousData,  queryFn: async () => await fetchPayments(search),  queryKey: ["payments", search],});
const table = useTable({ columns, data: query.data ?? noPayments, features });
<DataTable  empty="No payments match."  label="Payments"  query={query}  table={table}/>;

Pagination

Put DataTablePagination under the table and register rowPaginationFeature. On the client, add paginatedRowModel: createPaginatedRowModel() and it pages the rows itself. On a server, as here, set manualPagination and manualSorting, pass the total as rowCount, and put the pagination and sorting state in the queryKey.

Status
const query = useQuery({  placeholderData: keepPreviousData,  queryFn: async () => await fetchPage(pagination, sorting),  queryKey: ["payments", pagination, sorting],});
const table = useTable({  columns,  data: query.data?.rows ?? noPayments,  features: tableFeatures({ rowPaginationFeature, rowSortingFeature }),  manualPagination: true,  manualSorting: true,  onPaginationChange: setPagination,  onSortingChange: setSorting,  rowCount: query.data?.total ?? 0,  state: { pagination, sorting },});
<DataTable label="Payments" query={query} table={table} {...props} /><DataTablePagination table={table} />;

API Reference

PartAdds
DataTabletable, label, empty; onRowClick; activeRowId, the open row; query, a useQuery result; error, shown when it fails; columnSx, each column's width by id; rowHeight, a guess for the virtualizer; scrollRef, the element it scrolls in when not the window; size
DataTableColumnHeadercolumn, title; the sort icon leads in a column with meta: { align: "end" }
DataTablePaginationtable; pageSizes, the rows per page to offer, 10, 20, 50 and 100 by default
DataTableSelectAlltable
DataTableViewOptionstable; labels, each column's name by id
DataTableSelectRowrow; numbered, the row's number until it's pointed at or any row is selected