Shared UITable

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.

TanStack Table v9Base UIClient + server

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
BacklogLow
TASK-1002
DocumentationDocument the component composition patterns
TodoMedium
TASK-1003
BugReview keyboard navigation across overlays
In progressHigh
TASK-1004
FeatureSimplify the workspace onboarding flow
DoneLow
TASK-1005
DocumentationAlign empty states with the design system
CanceledMedium
TASK-1006
BugMake the task list easier to scan
BacklogHigh
TASK-1007
FeatureTest responsive behavior on smaller screens
TodoLow
TASK-1008
DocumentationPolish the final details before handoff
In progressMedium
TASK-1009
BugConnect the table to a paginated endpoint
DoneHigh
TASK-1010
FeatureCheck sorting and filtering together
CanceledLow
0 of 100 row(s) selected
Page 1 of 10

Changes stay in this demo and reset when you switch examples.

Client-owned data

tasks.tsx
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.

columns.tsx
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.

custom toolbar
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.

PropTypeDefault
label

Accessible name for the table region.

string—
columns

Built with createDataTableColumnHelper.

DataTableColumn<T>[]—
data

Rows for the current view. With ServerDataTable, only the current page.

T[]—
getRowId

Stable unique ID across every page, filter, sort, and refresh.

(row: T) => string—
search

Text filter bound to one column.

{ columnId; placeholder? }—
filters

Faceted filters. Options may carry a server-wide count.

DataTableFilter[]—
columnLabels

Names shown in the column visibility menu.

Record<string, string>—
selectable

Adds the selection column.

booleanfalse
pageSizes

Choices for rows per page.

number[]—
labels

Override any visible string for localization.

Partial<DataTableLabels>—
actions

Rendered at the end of the default toolbar.

ReactNode—
toolbar

Replace the toolbar. Return null to hide it.

(table) => ReactNode—
pagination

Replace the pagination. Return null to hide it.

(table) => ReactNode—
emptyContent

Shown when there are no rows and no active filters.

ReactNode—
loading

Shows the loading row and disables controls.

boolean—
error

Safe display message. Never pass raw provider errors.

string—
onRetry

Enables the retry action in the error state.

() => void—
PropTypeDefault
state

pagination, sorting, columnFilters, columnVisibility, and rowSelection. Required for ServerDataTable.

DataTableState—
onStateChange

Receives a value or functional updater, like React's setState.

(updater) => void—
initialState

ClientDataTable only. Seeds uncontrolled state.

DataTableInitialState—
rowCount

ServerDataTable only. Total matching rows across all pages after server filtering.

number—