A powerful and flexible data table component for displaying, filtering, sorting, and paginating tabular data.
Installation
Install the main component and dependencies:
pnpm dlx shadcn@latest add "@diceui/data-table"Filter and sort menu installs bundle a sortable component. Primitives come from Base UI.
Wrap your application with the NuqsAdapter for query state management:
import { NuqsAdapter } from "nuqs/adapters/next/app";
<NuqsAdapter>
<App />
</NuqsAdapter>Install the following optional components:
pnpm dlx shadcn@latest add "@diceui/data-table-sort-menu"pnpm dlx shadcn@latest add "@diceui/data-table-filter-menu"pnpm dlx shadcn@latest add "@diceui/data-table-command-filter-menu"pnpm dlx shadcn@latest add "@diceui/data-table-select-column"pnpm dlx shadcn@latest add "@diceui/data-table-actions-column"Layout
Import the components and compose them together:
import { DataTable } from "@/components/data-table/data-table";
import { DataTableSortMenu } from "@/components/data-table/data-table-sort-menu";
import { DataTableToolbar } from "@/components/data-table/data-table-toolbar";
import { useDataTable } from "@/hooks/use-data-table";
const { table } = useDataTable({
data,
columns,
pageCount,
});
return (
<DataTable table={table}>
<DataTableToolbar table={table}>
<DataTableSortMenu table={table} />
</DataTableToolbar>
</DataTable>
);For the advanced toolbar with a filter menu, swap the toolbar:
import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar";
import { DataTableFilterMenu } from "@/components/data-table/data-table-filter-menu";
return (
<DataTable table={table}>
<DataTableAdvancedToolbar table={table}>
<DataTableFilterMenu table={table} />
<DataTableSortMenu table={table} />
</DataTableAdvancedToolbar>
</DataTable>
);Walkthrough
Define columns with appropriate metadata:
import { Text, CalendarIcon, DollarSign } from "lucide-react";
import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header";
const columns = React.useMemo(() => [
{
// The column id defaults to the `accessorKey` and is the URL key of its filter
accessorKey: "title",
header: ({ column }) => <DataTableColumnHeader column={column} />,
cell: ({ row }) => <div>{row.getValue("title")}</div>,
// Define the column meta options for sorting, filtering, and view options
meta: {
label: "Title",
placeholder: "Search titles...",
variant: "text",
icon: Text,
},
// By default, the column will not be filtered. Set to `true` to enable filtering.
enableColumnFilter: true,
},
], []);Initialize the table state using the useDataTable hook:
import { useDataTable } from "@/hooks/use-data-table";
function DataTableDemo() {
const { table } = useDataTable({
data,
columns,
// Pass the total number of pages for server-side data,
// or set `mode: "client"` to sort, filter, and paginate in the browser
pageCount,
// Defaults used when the URL has no sorting or page size
config: {
defaultSorting: [{ id: "createdAt", desc: true }],
defaultPageSize: 10,
},
// Unique identifier for rows, can be used for unique row selection
getRowId: (row) => row.id,
});
return (
// ... render table
);
}Pass the table instance to the DataTable, and DataTableToolbar components:
import { DataTable } from "@/components/data-table/data-table";
import { DataTableToolbar } from "@/components/data-table/data-table-toolbar";
import { DataTableSortMenu } from "@/components/data-table/data-table-sort-menu";
function DataTableDemo() {
return (
<DataTable table={table}>
<DataTableToolbar table={table}>
<DataTableSortMenu table={table} />
</DataTableToolbar>
</DataTable>
);
}For advanced filtering, use the DataTableAdvancedToolbar component:
import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar";
import { DataTableFilterMenu } from "@/components/data-table/data-table-filter-menu";
import { DataTableSortMenu } from "@/components/data-table/data-table-sort-menu";
function DataTableDemo() {
return (
<DataTable table={table}>
<DataTableAdvancedToolbar table={table}>
<DataTableFilterMenu table={table} />
<DataTableSortMenu table={table} />
</DataTableAdvancedToolbar>
</DataTable>
);
}Alternatively, swap out DataTableFilterMenu with DataTableCommandFilterMenu for a command palette-style interface:
import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar";
import { DataTableFilterMenu } from "@/components/data-table/data-table-filter-menu";
import { DataTableCommandFilterMenu } from "@/components/data-table/data-table-command-filter-menu";
import { DataTableSortMenu } from "@/components/data-table/data-table-sort-menu";
function DataTableDemo() {
return (
<DataTable table={table}>
<DataTableAdvancedToolbar table={table}>
<DataTableFilterMenu table={table} />
<DataTableCommandFilterMenu table={table} />
<DataTableSortMenu table={table} />
</DataTableAdvancedToolbar>
</DataTable>
);
}Render an action bar on row selection:
import { ActionBar } from "@/components/ui/action-bar";
function TableActionBar({ table }: { table: Table<Data> }) {
const rows = table.getFilteredSelectedRowModel().rows;
const onOpenChange = React.useCallback((open: boolean) => {
if (!open) {
table.toggleAllRowsSelected(false);
}
},
[table],
);
return (
<ActionBar open={rows.length > 0} onOpenChange={onOpenChange}>
{/* Add your custom actions here */}
</ActionBar>
);
}
function DataTableDemo() {
return (
<DataTable
table={table}
actionBar={<TableActionBar table={table} />}
>
<DataTableToolbar table={table} />
</DataTable>
);
}Row Selection
Use getDataTableSelectColumn to add a checkbox column for selecting rows:
import { getDataTableSelectColumn } from "@/components/data-table/data-table-select-column";
const columns = React.useMemo<ColumnDef<DataTableFeatures, Project>[]>(
() => [
getDataTableSelectColumn(),
// ...other columns
],
[],
);Cells are memoized and only re-render when the state they subscribe to changes. If you render your own selection-dependent cell, read the selection inside Subscribe instead of calling row.getIsSelected() directly:
import { Subscribe } from "@tanstack/react-table";
cell: ({ row }) => (
<Subscribe
source={row.table.atoms.rowSelection}
selector={(selection) => selection[row.id] === true}
>
{(isSelected) => (
<Checkbox
checked={isSelected}
onCheckedChange={(value) => row.toggleSelected(!!value)}
/>
)}
</Subscribe>
),Press Escape inside the table to clear the row selection. Call event.preventDefault() in onEscapeKeyDown to keep it.
Row Actions
Use getDataTableActionsColumn to add a menu of actions to each row:
import { getDataTableActionsColumn } from "@/components/data-table/data-table-actions-column";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
const columns = React.useMemo<ColumnDef<DataTableFeatures, Project>[]>(
() => [
// ...other columns
getDataTableActionsColumn({
actions: ({ row }) => (
<DropdownMenuItem onClick={() => onRowDelete(row.original)}>
Delete
</DropdownMenuItem>
),
}),
],
[onRowDelete],
);Pin it to the end with initialState.columnPinning.end, like { start: ["select"], end: ["actions"] }.
Sticky Header
Set stickyHeader to scroll the rows inside the table and keep the header in view. Set the height with className:
<DataTable table={table} stickyHeader className="max-h-[600px]">
<DataTableToolbar table={table} />
</DataTable>Client and Server Mode
useDataTable runs in one of two modes. Both keep pagination, sorting, and filters in the URL.
| Title | Description |
|---|---|
server | Default. Pass the current page of `data` and the total `pageCount` (required). Read the URL state on the server and query your database. |
client | Pass all rows as `data` and omit `pageCount`. The hook sorts, filters, and paginates them in the browser and derives the page count. |
pageCount is required in server mode and not allowed in client mode. TypeScript reports an error if you pass it in client mode or leave it out in server mode.
Client Mode
Use client mode when the full dataset fits in the browser:
const { table } = useDataTable({
mode: "client",
data,
columns,
getRowId: (row) => row.id,
});Server Mode
Use server mode for large datasets. Describe the table once with createDataTableConfig, and share that config between the server and useDataTable.
Create the table config in a file both the server and the client can import:
import type { DataTableColumnConfigs } from "@/lib/data-table-types";
import { createDataTableConfig } from "@/lib/data-table-config";
export const tasksTableConfig = createDataTableConfig({
columns: {
// Sortable, not filterable
code: {},
// Filterable, not sortable
title: { variant: "text", enableSorting: false },
status: { variant: "select" },
estimatedHours: { variant: "number" },
createdAt: { variant: "date" },
} satisfies DataTableColumnConfigs<Task>,
defaultSorting: [{ id: "createdAt", desc: true }],
});The config's columns is an allowlist: a column it doesn't list can't be filtered or sorted, even from the URL. variant turns on filtering for the column, and enableSorting: false turns off sorting. DataTableColumnConfigs<Task> checks the column ids against your row type.
Parse the search params on the server and turn them into a query:
export default async function Page(props: {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
// { page, perPage, sorting, filters, joinOperator, globalFilter }
const query = tasksTableConfig.parse(await props.searchParams);
const { data, pageCount } = await getTasks(query);
return <TasksTable data={data} pageCount={pageCount} />;
}Type the query function's input with InferDataTableQuery<typeof tasksTableConfig> from @/lib/data-table-config. Each filter has an id, variant, operator, and value to map onto your database query.
Pass the config, the server data, and pageCount to useDataTable. The config sets each column's filter and sorting, so the column definitions don't need enableColumnFilter or meta.variant. Pass startTransition from React.useTransition() and set shallow to false to show loading states while the server re-renders:
import { tasksTableConfig } from "@/lib/validations";
const [isPending, startTransition] = React.useTransition();
const { table } = useDataTable({
config: tasksTableConfig,
data,
columns,
pageCount,
shallow: false,
startTransition,
});Global Filter
The global filter is stored in the URL as ?search= and comes back from config.parse as globalFilter. Pass globalFilterPlaceholder to DataTableToolbar or DataTableAdvancedToolbar to render a search input for it:
<DataTableToolbar table={table} globalFilterPlaceholder="Search tasks..." />In client mode, TanStack Table's global filter runs over the rows. Set enableGlobalFilter: false in the config to turn it off.
Multiple Tables
Give each config an id to render several tables on one page. The id prefixes every URL key, so the tables don't share state:
export const tasksTableConfig = createDataTableConfig({
id: "tasks",
columns,
});
// ?tasks.page=2&tasks.status=todoURL Format
Each filterable column has its own search param, keyed by column id. The value is the operator followed by the filter value. When a filter uses the column's default operator, the operator is left out.
?title=report # text, default operator (contains)
?title=eq.Weekly%20report # text, "is"
?status=todo,done # select, default operator (has any of)
?estimatedHours=2,8 # number, two values (between)
?estimatedHours=gt.4 # number, "is greater than"
?createdAt=is.empty # no value
?sort=createdAt.desc,title.asc
?joinOperator=or
?search=report # global filterAPI Reference
Column Definitions
The column definitions are used to define the columns of the data table.
const columns = React.useMemo<ColumnDef<Project>[]>(() => [
{
// Required: Key to access the data, also used as the column id
accessorKey: "title",
// Optional: Custom header component
header: ({ column }) => <DataTableColumnHeader column={column} />,
// Optional: Custom cell component
cell: ({ row }) => <div>{row.getValue("title")}</div>,
// Optional: Meta options for filtering, sorting, and view options
meta: {
label: "Title",
placeholder: "Search titles...",
variant: "text",
icon: Text,
},
// By default, the column will not be filtered. Set to `true` to enable filtering.
enableColumnFilter: true,
},
{
// Required with `accessorFn`: Unique identifier for the column
id: "status",
// Access nested data using `accessorFn`
accessorFn: (row) => row.lineItem.status,
header: "Status",
meta: {
label: "Status",
variant: "select",
options: [
{ label: "Active", value: "active" },
{ label: "Inactive", value: "inactive" },
],
},
enableColumnFilter: true,
},
], []);Properties
Core configuration options for defining columns.
| Prop | Description |
|---|---|
id | Unique identifier for the column, used as its URL key. Defaults to `accessorKey`; required with `accessorFn` |
accessorKey | Required: Key to access the data from the row |
accessorFn | Optional: Custom accessor function to access data |
header | Optional: Custom header component with column props |
cell | Optional: Custom cell component with row props |
meta | Optional: Meta options for accessing column metadata |
enableColumnFilter | By default, the column will not be filtered. Set to `true` to enable filtering. A config from `createDataTableConfig` sets this from its `variant` |
enableSorting | Enable sorting for this column. A config from `createDataTableConfig` sets this from its `enableSorting` |
enableHiding | Enable column visibility toggle |
Column Meta
Column meta options for filtering, sorting, and view options.
| Prop | Description |
|---|---|
label | The display name for the column |
placeholder | The placeholder text for filter inputs |
variant | The column's data type, which picks its filter (`text`, `number`, `date`, `boolean`, `select`). A config from `createDataTableConfig` sets this |
options | For `select` filters, an array of options with `label`, `value`, and optional `count` and `icon` |
multiple | For `select` filters, whether the toolbar filter can pick more than one option. Defaults to `true` |
min | For `number` filters, the lower bound of the toolbar slider |
max | For `number` filters, the upper bound of the toolbar slider |
unit | For numeric filters, the unit to display (e.g., 'hr', '$') |
icon | The react component to use as an icon for the column |
Filter Variants
Available variants for the table config and column meta.
| Title | Description |
|---|---|
text | Text search with contains, equals, etc. |
number | Numeric filters with equals, greater than, less than, between, etc. The toolbar shows a slider when the column sets `min` and `max`, or in client mode. |
date | Date filters with equals, before, after, between, etc. The toolbar picks a start and end date. |
boolean | Boolean filters with true/false values |
select | Filters with predefined options. Set `multiple: false` in the column meta to pick one option in the toolbar. |
Reference the TanStack Table Column Definitions Guide for detailed column definition guide.
createDataTableConfig
Creates the table config shared by the server and useDataTable. Read the URL on the server with config.parse(searchParams).
Prop
Type
useDataTable
A hook for initializing the data table with state management.
Prop
Type
DataTable
The main data table component.
Prop
Type
getDataTableActionsColumn
A utility function that returns an actions column with a dropdown menu for each row.
Prop
Type
DataTableColumnHeader
Custom header component for columns with sorting.
Prop
Type
DataTableToolbar
Standard toolbar with filtering and view options.
Prop
Type
DataTableAdvancedToolbar
Advanced toolbar with more comprehensive filtering capabilities.
Prop
Type
DataTableViewOptions
Controls column visibility and display preferences in the data table.
Prop
Type
DataTableSortMenu
List of applied sorting with ability to add, remove, and modify sorting.
Prop
Type
DataTableFilterMenu
List of applied filters with ability to add, remove, and modify filters.
Prop
Type
DataTableCommandFilterMenu
Command palette-style filter menu with ability to add, remove, and modify filters.
Prop
Type
DataTablePagination
Pagination controls for the data table.
Prop
Type
Accessibility
Keyboard Interactions
| Key | Description |
|---|---|
| Ctrl + Shift + FCmd + Shift + F | Toggles the filter menu. |
| Ctrl + Shift + SCmd + Shift + S | Toggles the sort menu. |
| BackspaceDelete | Removes the focused filter/sort item. Removes the last applied filter/sort when menu trigger is focused. |
| Escape | Clears the row selection when focus is inside the table. |
Features
- Advanced Filtering - Multiple filter types (text, number, date, boolean, select) with customizable operators
- Shared Config - One config per table for the server and the client, with an optional id for multiple tables on a page
- URL State Management - Sync pagination, sorting, filters, and the global filter with URL search params using nuqs
- Sorting - Multi-column sorting with persistent state
- Sticky Header - Keep the header in view while the rows scroll
- Pagination - Server-side or client-side pagination with customizable page sizes
- Column Visibility - Toggle column visibility with view options menu
- Row Selection - Single or multi-row selection with action bar
- Column Pinning - Pin columns to left or right for better UX
- Keyboard Navigation - Full keyboard support with shortcuts for filter and sort menus
- Responsive - Mobile-first design with overflow handling
- Customizable - Flexible API for custom filters, columns, and actions
Credits
- shadcn/ui - For the initial implementation of the data table.