Data Table
A fully-featured data table built on TanStack Table with advanced filtering, sorting, pagination, column pinning, row selection, and mobile card view.
A heavily extended data table built on TanStack Table and shadcn/ui. State (filters, sorting, pagination) is synced to the URL via nuqs, making every table view shareable and bookmarkable.
Installation
Install the component via the shadcn CLI:
npx shadcn add @qotra/data-tableThis adds the following files to your project:
components/data-table/— all table componentshooks/use-data-table.ts— the core table state hooklib/data-table.ts— utility functionslib/parsers.ts— nuqs parsers for URL stateconfig/data-table.ts— filter/sort operator configtypes/data-table.ts— shared TypeScript types
Wrap your app with NuqsAdapter
URL state management requires the nuqs adapter at your app root:
import { NuqsAdapter } from 'nuqs/adapters/react-router/v7';
export default function Root() {
return (
<NuqsAdapter>
<App />
</NuqsAdapter>
);
}Dependencies
npm packages: @tanstack/react-table, nuqs, nanoid, zod, react-day-picker, lucide-react
shadcn components: table, button, input, select, badge, skeleton, calendar, popover, command, dropdown-menu, separator, slider, label
Usage
1. Define columns
Each column needs an id, an accessorKey (or accessorFn), and a meta block describing how it should be filtered and displayed.
import type { ColumnDef } from '@tanstack/react-table';
import { Text, CalendarIcon, Tag } from 'lucide-react';
import { DataTableColumnHeader } from '@/components/data-table/data-table-column-header';
interface Task {
id: string;
title: string;
status: 'todo' | 'in-progress' | 'done';
priority: 'low' | 'medium' | 'high';
createdAt: Date;
}
const columns: ColumnDef<Task>[] = [
{
id: 'title',
accessorKey: 'title',
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Title" />
),
cell: ({ row }) => <span>{row.getValue('title')}</span>,
meta: {
label: 'Title',
placeholder: 'Search titles...',
variant: 'text',
icon: Text,
},
enableColumnFilter: true,
},
{
id: 'status',
accessorKey: 'status',
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Status" />
),
meta: {
label: 'Status',
variant: 'multiSelect',
icon: Tag,
options: [
{ label: 'Todo', value: 'todo' },
{ label: 'In Progress', value: 'in-progress' },
{ label: 'Done', value: 'done' },
],
},
enableColumnFilter: true,
},
{
id: 'createdAt',
accessorKey: 'createdAt',
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Created" />
),
meta: {
label: 'Created At',
variant: 'dateRange',
icon: CalendarIcon,
},
enableColumnFilter: true,
},
];2. Initialize the table
Use the useDataTable hook to manage all table state — pagination, sorting, and filtering are automatically synced to the URL.
import { useDataTable } from '@/hooks/use-data-table';
function TasksTable({ data, pageCount }: { data: Task[]; pageCount: number }) {
const { table } = useDataTable({
data,
columns,
pageCount,
initialState: {
sorting: [{ id: 'createdAt', desc: true }],
pagination: { pageSize: 20 },
},
getRowId: (row) => row.id,
});
return (
<DataTable table={table}>
<DataTableToolbar table={table}>
<DataTableSortList table={table} />
</DataTableToolbar>
</DataTable>
);
}3. Render the table
import { DataTable } from '@/components/data-table/data-table';
import { DataTableToolbar } from '@/components/data-table/data-table-toolbar';
import { DataTableSortList } from '@/components/data-table/data-table-sort-list';
<DataTable table={table}>
<DataTableToolbar table={table}>
<DataTableSortList table={table} />
</DataTableToolbar>
</DataTable>Advanced toolbar with filter list
Swap in DataTableAdvancedToolbar and DataTableFilterList for a persistent filter row:
import { DataTableAdvancedToolbar } from '@/components/data-table/data-table-advanced-toolbar';
import { DataTableFilterList } from '@/components/data-table/data-table-filter-list';
import { DataTableSortList } from '@/components/data-table/data-table-sort-list';
<DataTable table={table}>
<DataTableAdvancedToolbar table={table}>
<DataTableFilterList table={table} />
<DataTableSortList table={table} />
</DataTableAdvancedToolbar>
</DataTable>Make sure to pass enableAdvancedFilter: true to useDataTable when using advanced filtering:
const { table } = useDataTable({
data,
columns,
pageCount,
enableAdvancedFilter: true,
});Command-palette filter menu
Replace DataTableFilterList with DataTableFilterMenu for a keyboard-driven command palette interface:
import { DataTableFilterMenu } from '@/components/data-table/data-table-filter-menu';
<DataTable table={table}>
<DataTableAdvancedToolbar table={table}>
<DataTableFilterMenu table={table} />
<DataTableSortList table={table} />
</DataTableAdvancedToolbar>
</DataTable>Bulk action bar
Show a floating action bar when rows are selected:
import { BulkActionBar } from '@/components/data-table/bulk-action-bar';
import { Button } from '@/components/ui/button';
function TaskActionBar({ table }: { table: Table<Task> }) {
const rows = table.getFilteredSelectedRowModel().rows;
return (
<BulkActionBar
count={rows.length}
noun="task"
onClear={() => table.toggleAllRowsSelected(false)}
>
<Button
variant="outline"
size="sm"
onClick={() => handleDelete(rows.map((r) => r.original.id))}
>
Delete
</Button>
</BulkActionBar>
);
}
<DataTable table={table} actionBar={<TaskActionBar table={table} />}>
<DataTableToolbar table={table} />
</DataTable>Loading state
Pass isLoading to show skeleton rows while data is fetching:
<DataTable table={table} isLoading={isFetching}>
<DataTableToolbar table={table} />
</DataTable>Mobile infinite scroll
On mobile, the table automatically switches to a card layout. Pass hasMore and onLoadMore to enable infinite scroll:
<DataTable
table={table}
hasMore={hasNextPage}
onLoadMore={fetchNextPage}
>
<DataTableToolbar table={table} />
</DataTable>Server-side data fetching
The hook exposes parsed URL state values you can pass directly to your server or API:
const { table, page, perPage, sorting, filterValues } = useDataTable({
data,
columns,
pageCount,
enableAdvancedFilter: true,
});
// Use these in your data fetching (e.g. with React Router loaders):
// page → current page number
// perPage → page size
// sorting → [{ id: 'createdAt', desc: true }]
// filterValues → advanced filter state from the URLWith React Router, read the URL params directly in your loader:
export async function loader({ request }: Route.LoaderArgs) {
const url = new URL(request.url);
const page = Number(url.searchParams.get('page') ?? 1);
const perPage = Number(url.searchParams.get('perPage') ?? 20);
const sort = url.searchParams.get('sort');
const { data, total } = await getTasks({ page, perPage, sort });
return { data, pageCount: Math.ceil(total / perPage) };
}API Reference
useDataTable
The core hook for initializing table state. All pagination, sorting, and filter state is synced to the URL automatically.
| Prop | Type | Default | Description |
|---|---|---|---|
data | TData[] | — | The row data to display |
columns | ColumnDef<TData>[] | — | Column definitions |
pageCount | number | — | Total number of pages (required for server-side pagination) |
initialState | Partial<TableState> | — | Initial sorting, pagination, and visibility state |
getRowId | (row: TData) => string | — | Function to derive a unique ID per row, used for stable row selection |
queryKeys | Partial<QueryKeys> | — | Override default URL param names (page, perPage, sort, filters, joinOperator) |
history | 'push' | 'replace' | 'replace' | Whether URL changes push a new history entry or replace the current one |
debounceMs | number | 300 | Debounce delay for filter input changes |
throttleMs | number | 50 | Throttle delay for URL state writes |
clearOnDefault | boolean | false | Remove URL params when values match their defaults |
enableAdvancedFilter | boolean | false | Enable advanced filter mode (required for DataTableFilterList / DataTableFilterMenu) |
scroll | boolean | false | Scroll to top on pagination change |
shallow | boolean | true | Use shallow routing for URL updates |
startTransition | React.TransitionStartFunction | — | Wrap URL state updates in a React transition |
Returns: { table, page, perPage, sorting, filterValues, shallow, debounceMs, throttleMs }
DataTable
The main table container. Renders a standard table on desktop and switches to a card list on mobile.
| Prop | Type | Default | Description |
|---|---|---|---|
table | Table<TData> | — | The table instance from useDataTable |
actionBar | React.ReactNode | — | Content to render in the bulk action bar slot |
isLoading | boolean | false | Show skeleton rows instead of data |
hasMore | boolean | — | Whether more rows can be loaded (mobile infinite scroll) |
onLoadMore | () => void | — | Called when the user scrolls to the bottom on mobile |
DataTableToolbar
Standard toolbar with a search input and column visibility toggle. Place sort/filter controls as children.
| Prop | Type | Description |
|---|---|---|
table | Table<TData> | The table instance |
children | React.ReactNode | Additional toolbar controls (e.g. DataTableSortList) |
DataTableAdvancedToolbar
Extended toolbar designed to host DataTableFilterList or DataTableFilterMenu alongside sort controls.
| Prop | Type | Description |
|---|---|---|
table | Table<TData> | The table instance |
children | React.ReactNode | Filter and sort components |
DataTableFilterList
Renders active filters as removable chips with inline editors. Supports adding new filters from a dropdown.
| Prop | Type | Description |
|---|---|---|
table | Table<TData> | The table instance |
DataTableFilterMenu
A command palette-style interface for adding and editing filters. Opened with Cmd/Ctrl + Shift + F.
| Prop | Type | Description |
|---|---|---|
table | Table<TData> | The table instance |
DataTableSortList
Renders active sorts as removable chips, with a dropdown to add new sort rules. Supports drag-to-reorder. Opened with Cmd/Ctrl + Shift + S.
| Prop | Type | Description |
|---|---|---|
table | Table<TData> | The table instance |
DataTableColumnHeader
Drop-in column header with sort toggle and hide controls.
| Prop | Type | Description |
|---|---|---|
column | Column<TData> | The TanStack column instance |
title | string | Display label |
DataTablePagination
Page size selector and page navigation controls. Included inside DataTable by default — only use this directly if you need a custom layout.
| Prop | Type | Description |
|---|---|---|
table | Table<TData> | The table instance |
DataTableViewOptions
Dropdown to toggle column visibility. Included in DataTableToolbar — use directly for custom toolbar layouts.
| Prop | Type | Description |
|---|---|---|
table | Table<TData> | The table instance |
BulkActionBar
A fixed floating bar that appears when rows are selected. Renders via a React portal so it sits above all other content.
| Prop | Type | Default | Description |
|---|---|---|---|
count | number | — | Number of selected rows. Bar is hidden when 0 |
noun | string | — | Singular noun for the item type (e.g. 'task') — pluralized automatically |
onClear | () => void | — | Called when the user clicks the clear button |
loading | boolean | false | Show a loading spinner instead of the count |
children | React.ReactNode | — | Action buttons |
Column meta
The meta field on each column definition controls filtering and display behavior.
| Field | Type | Description |
|---|---|---|
label | string | Display name used in filter UI and sort list |
placeholder | string | Placeholder text for text filter inputs |
variant | FilterVariant | Which filter type to use (see below) |
options | Option[] | Options for select and multiSelect filters |
range | [number, number] | Min/max bounds for range and slider filters |
unit | string | Unit suffix for numeric filters (e.g. 'hr', '$') |
icon | React.FC | Icon component shown alongside the column name |
Filter variants
| Variant | Description |
|---|---|
text | Text search — contains, equals, is empty, etc. |
number | Numeric — equals, greater than, less than, between, etc. |
range | Dual-input numeric range with min and max |
date | Date picker — before, after, on, between, relative to today |
dateRange | Date range picker with start and end |
boolean | True / false toggle |
select | Single option from a predefined list |
multiSelect | One or more options from a predefined list |
Types
Option
interface Option {
label: string;
value: string;
count?: number;
icon?: React.FC<React.SVGProps<SVGSVGElement>>;
}QueryKeys
Override the default URL parameter names used for table state.
interface QueryKeys {
page: string; // default: 'page'
perPage: string; // default: 'perPage'
sort: string; // default: 'sort'
filters: string; // default: 'filters'
joinOperator: string; // default: 'joinOperator'
}DataTableRowAction
A helper type for typed row action handlers.
interface DataTableRowAction<TData> {
row: Row<TData>;
variant: 'update' | 'delete';
}Keyboard shortcuts
| Shortcut | Action |
|---|---|
Cmd/Ctrl + Shift + F | Toggle filter menu |
Cmd/Ctrl + Shift + S | Toggle sort menu |
Backspace / Delete | Remove focused filter or sort item; removes the last item when the menu trigger is focused |