Data Table
Search, filter, sort, select, and act on rows of application data.
Overview
Use Data Table for records that people need to find and act on. Invoice queues and customer directories often need search, status filters, row selection, and pagination. Data Table adds those controls to Table.
Use Table when you only need to present rows and columns. Add Data Table when users need to narrow the records, change which columns they see, or select rows for an action.
Anatomy
DataTable
├── Toolbar: search, filters, columns, actions
├── Content: header row, data rows, states
└── Footer: selection summary, paginationDataTable renders all three regions. Data Table also exports each part separately: DataTableToolbar, DataTableSearch, DataTableFilter, DataTableViewOptions, DataTableContent, DataTableFooter, DataTableSelectionSummary, and DataTablePagination. Use the separate parts to change their order or add controls.
The rows render in a native HTML table with <thead>, <th scope="col">, and <tbody> elements. Column headers stay visible during vertical scrolling. Wide tables scroll horizontally instead of turning each row into a card.
Behavior
Data Table handles sorting, filters, column visibility, selection, and pagination in the browser by default. To handle one of these on your server, pass its controlled state and callback. Set manualSorting, manualFiltering, or manualPagination for the matching task.
Keep each task in one place. If the server paginates 10,000 records, it must also sort them. Sorting the ten rows in the browser only sorts the current page and gives the wrong result.
Search updates on every change unless you set debounceMs. A filter can include several values from the same column. The Columns menu lists columns that TanStack Table allows users to hide. When users select rows, the footer reports the count and provides a Clear action.
Accessibility
Use caption to name the table. Screen readers announce this name before the cells. Sortable headers render as buttons with names such as "Sort by Customer." The <th> element reports the current direction through aria-sort.
Use getRowLabel to name each selection checkbox. Return a phrase such as "Select Olivia Rhye" instead of a row number. The pagination range uses aria-live. Page controls report their state through aria-current and aria-disabled.
Do not use color as the only status cue. A green status badge should also say "Paid."
Installation
Usage
Pass the columns and rows. Then turn on search, selection, or pagination as needed:
import { DataTable } from "@/components/ui/data-table/data-table"
const columns = [
{ accessorKey: "name", header: "Customer" },
{ accessorKey: "email", header: "Email" },
{
accessorKey: "spent",
header: "Spent",
meta: { align: "right" },
},
]
<DataTable
columns={columns}
data={customers}
caption="Customers"
search={{ placeholder: "Search customers..." }}
selectable
pagination={{ pageSizeOptions: [10, 20, 50] }}
/>Columns use TanStack Table's ColumnDef format. A cell can contain any React content, including a link, badge, menu, or formatted number. Set meta.align: "right" on numeric columns so the digits line up.
Server-side mode
When rows come from an API, keep search, sorting, filters, and pagination state in your application. Pass that state to Data Table and use each callback to request new rows. Set the matching manual* flags and pass rowCount. Pass pageCount too if the server already calculates it.
This example handles search, sorting, and pagination on the server:
<DataTable
columns={columns}
data={query.data.rows}
rowCount={query.data.total}
loading={query.isFetching}
search={{ placeholder: "Search orders...", debounceMs: 250 }}
manualSorting
manualFiltering
manualPagination
sorting={sorting}
onSortingChange={setSorting}
globalFilter={globalFilter}
onGlobalFilterChange={setGlobalFilter}
pagination={pagination}
onPaginationChange={setPagination}
/>Don't do this
Treating no results as no data
// Bad. This hides the recovery path.
{rows.length === 0 ? <p>Nothing here yet</p> : null}// Good. Each state has its own message.
<DataTable
columns={columns}
data={rows}
emptyState={<EmptyAddCustomer />}
// No-results state offers Clear filters automatically
/>An empty dataset has no records. A no-results state still has records, but the current search or filters exclude them. Use separate messages so users know whether to add a record or clear the current view.
Examples
Search and filters
Search across the table or filter by status. The Filters button reports how many filters are active and includes a Clear all action. Choose filters that exclude every row to see the no-results state.
Selection and bulk actions
Select one row or all rows on the page. The footer reports the selection count and clears it. The example connects selected rows to bulk actions and gives each row an actions menu.
Custom cells
Render React content inside cells. This example uses avatars, status badges, right-aligned currency, and formatted dates.
Loading
Skeleton rows replace the table body while data loads. The toolbar, header, and footer stay in place.
Error
When a request fails, the table shows the error and a Try again action instead of a no-results message.
Empty data
Use emptyState when the dataset has no records. Its message should explain how to add the first one.
Controlled server table
This example sends search, sorting, filters, and pagination changes to the server. It debounces search input and shows a loading state during each request.
API reference
DataTable
Controlled state follows TanStack Table's value and callback convention. The pairs are sorting and onSortingChange, globalFilter and onGlobalFilterChange, columnFilters and onColumnFiltersChange, columnVisibility and onColumnVisibilityChange, and rowSelection and onRowSelectionChange. Pagination uses pagination with onPaginationChange. If you omit a controlled value, Data Table manages that state.