Data Table.
One shared surface, two ways to own your data. Ready-to-use tables with composable controls for the details that make yours different.
In practice
Three ownership models, one table.
The client table manages its own state. The controlled client table lets your app own state for persistence or URL sync. The server table hands pagination, sorting, and filtering to your API and shows only the current page.
A little structure. A clear next step.
Your team’s work, with room to focus. 100 tasks in this local demo.
| Task | |||||
|---|---|---|---|---|---|
| TASK-1001 | FeatureDefine the foundations for the next release | Backlog | Low | ||
| TASK-1002 | DocumentationDocument the component composition patterns | Todo | Medium | ||
| TASK-1003 | BugReview keyboard navigation across overlays | In progress | High | ||
| TASK-1004 | FeatureSimplify the workspace onboarding flow | Done | Low | ||
| TASK-1005 | DocumentationAlign empty states with the design system | Canceled | Medium | ||
| TASK-1006 | BugMake the task list easier to scan | Backlog | High | ||
| TASK-1007 | FeatureTest responsive behavior on smaller screens | Todo | Low | ||
| TASK-1008 | DocumentationPolish the final details before handoff | In progress | Medium | ||
| TASK-1009 | BugConnect the table to a paginated endpoint | Done | High | ||
| TASK-1010 | FeatureCheck sorting and filtering together | Canceled | Low |
Changes stay in this demo and reset when you switch examples.
Client-owned data
import { ClientDataTable, createDataTableColumnHelper } from "@/components/ui/data-table";
const helper = createDataTableColumnHelper<Task>();
const columns = helper.columns([
helper.accessor("title", { header: "Title" }),
helper.accessor("status", { header: "Status", filterFn: "includesAny" }),
]);
export function Tasks({ tasks }: { tasks: Task[] }) {
return (
<ClientDataTable
label="Tasks"
columns={columns}
data={tasks}
getRowId={(task) => task.id}
search={{ columnId: "title", placeholder: "Filter tasks…" }}
filters={[{ columnId: "status", title: "Status", options: statuses }]}
selectable
/>
);
}Columns
Defining columns
The column helper is a thin wrapper over TanStack's, typed to the shared feature set. Sortable headers, faceted filters, and row actions are ordinary column definitions.
import {
createDataTableColumnHelper,
DataTableColumnHeader,
DataTableRowActions,
} from "@/components/ui/data-table";
const helper = createDataTableColumnHelper<Task>();
export const columns = helper.columns([
helper.accessor("id", { header: "Task", enableSorting: false, enableHiding: false }),
helper.accessor("title", {
header: ({ column }) => <DataTableColumnHeader column={column} title="Title" />,
cell: ({ row }) => <span className="truncate">{row.original.title}</span>,
}),
// Faceted columns use the shared multi-value filter.
helper.accessor("status", {
filterFn: "includesAny",
header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
}),
helper.display({
id: "actions",
cell: ({ row }) => (
<DataTableRowActions
row={row.original}
label={`Actions for ${row.original.id}`}
actions={[
{ id: "complete", label: "Mark complete", onSelect: completeTask },
{ id: "delete", label: "Delete", variant: "destructive", onSelect: deleteTask },
]}
/>
),
}),
]);Composition
Defaults where you need them. Space where you don't.
Pass search, filters, columnLabels, actions, pageSizes, and labels to configure the defaults. Replace toolbar or pagination with a render function, or return null to hide one. DataTable, DataTableToolbar, DataTableFacetedFilter, DataTableColumnHeader, DataTableViewOptions, DataTablePagination, and DataTableRowActions are all exported for custom compositions.
import { DataTableToolbar } from "@/components/ui/data-table";
<ClientDataTable
{...props}
// Replace a part without replacing the table. Return null to hide it.
toolbar={(table) => (
<DataTableToolbar
table={table}
search={{ columnId: "title" }}
actions={<Button onClick={createTask}>New task</Button>}
/>
)}
pagination={null}
emptyContent={<EmptyState onCreate={createTask} />}
/>Guarantees
Behavior
- Sorting, filter, and page size changes reset to page one. Filtering clears selection.
- Page changes preserve selected IDs. The header checkbox selects the current page only.
- With server data, option counts must come from the API. The table never presents counts from one page as global totals.
- The app owns authorization, persistence, request cancellation, and server page reconciliation after deletes or changing totals.
- Row action callbacks receive your original record, never a table row.
Reference
Props
ClientDataTable and ServerDataTable share the view props below. The second table lists what differs between the two.
| Prop | Type | Default |
|---|---|---|
labelAccessible name for the table region. | string | — |
columnsBuilt with createDataTableColumnHelper. | DataTableColumn<T>[] | — |
dataRows for the current view. With ServerDataTable, only the current page. | T[] | — |
getRowIdStable unique ID across every page, filter, sort, and refresh. | (row: T) => string | — |
searchText filter bound to one column. | { columnId; placeholder? } | — |
filtersFaceted filters. Options may carry a server-wide count. | DataTableFilter[] | — |
columnLabelsNames shown in the column visibility menu. | Record<string, string> | — |
selectableAdds the selection column. | boolean | false |
pageSizesChoices for rows per page. | number[] | — |
labelsOverride any visible string for localization. | Partial<DataTableLabels> | — |
actionsRendered at the end of the default toolbar. | ReactNode | — |
toolbarReplace the toolbar. Return null to hide it. | (table) => ReactNode | — |
paginationReplace the pagination. Return null to hide it. | (table) => ReactNode | — |
emptyContentShown when there are no rows and no active filters. | ReactNode | — |
loadingShows the loading row and disables controls. | boolean | — |
errorSafe display message. Never pass raw provider errors. | string | — |
onRetryEnables the retry action in the error state. | () => void | — |
| Prop | Type | Default |
|---|---|---|
statepagination, sorting, columnFilters, columnVisibility, and rowSelection. Required for ServerDataTable. | DataTableState | — |
onStateChangeReceives a value or functional updater, like React's setState. | (updater) => void | — |
initialStateClientDataTable only. Seeds uncontrolled state. | DataTableInitialState | — |
rowCountServerDataTable only. Total matching rows across all pages after server filtering. | number | — |