DataTable
Reference for the data table hook, components, column options and filters
Overview
shadcntable is built on TanStack Table v9 and shadcn/ui. The hook owns the table state. Each component takes the table instance, so you can place, restyle or replace them one by one.
| Export | File | Purpose |
|---|---|---|
useDataTable | use-data-table.ts | Creates the table instance |
DataTable | data-table.tsx | Renders the header and rows |
DataTableToolbar | data-table-toolbar.tsx | Global search, filter reset, view options |
DataTablePagination | data-table-pagination.tsx | Page navigation and page size |
DataTableColumnHeader | data-table-column-header.tsx | Sort and hide menu, filter popover |
DataTableViewOptions | data-table-view-options.tsx | Column visibility menu |
createSelectionColumn | data-table-selection-column.tsx | Checkbox column for row selection |
createDataTableColumnHelper | lib/features.ts | Typed column helper |
DataTableLocaleProvider | lib/locale.tsx | Translations |
Installation
pnpm dlx shadcn@latest add https://shadcntable.com/r/data-table.jsonUsage
'use client'
import { DataTable } from '@/components/shadcntable/data-table'
import { DataTableColumnHeader } from '@/components/shadcntable/data-table-column-header'
import { DataTablePagination } from '@/components/shadcntable/data-table-pagination'
import { DataTableToolbar } from '@/components/shadcntable/data-table-toolbar'
import { createDataTableColumnHelper } from '@/components/shadcntable/lib/features'
import { useDataTable } from '@/components/shadcntable/use-data-table'
type User = { id: string; name: string; email: string }
const columnHelper = createDataTableColumnHelper<User>()
const columns = columnHelper.columns([
columnHelper.accessor('name', {
header: ({ column }) => <DataTableColumnHeader column={column} />,
meta: { label: 'Name' },
}),
columnHelper.accessor('email', {
header: ({ column }) => <DataTableColumnHeader column={column} />,
meta: { label: 'Email' },
}),
])
export function UsersTable({ users }: { users: User[] }) {
const table = useDataTable({ columns, data: users, getRowId: (user) => user.id })
return (
<div className='space-y-4'>
<DataTableToolbar table={table} />
<DataTable table={table} />
<DataTablePagination table={table} />
</div>
)
}useDataTable
useDataTable wraps TanStack Table's useTable and registers the features the components need: sorting, column and global filtering, pagination, row selection and column visibility. It accepts every TanStack Table option. The most common ones are below.
| Prop | Type | Default | Description |
|---|---|---|---|
| data | TData[] | Required | Rows to display. Keep the reference stable between renders. |
| columns | DataTableColumnDef<TData>[] | Required | Column definitions, usually built with createDataTableColumnHelper. |
| initialState | Partial<TableState> | - | Starting state, e.g. the page size or a default sort. |
| state | Partial<TableState> | - | Controlled state slices. Pair each one with its on[Slice]Change. |
| manualPagination / manualSorting / manualFiltering | boolean | false | Leave that step to your server and render data as given. |
| rowCount | number | - | Total rows on the server, used for the page count in manual mode. |
| enableRowSelection | boolean | (row) => boolean | true | Which rows can be selected. |
| getRowId | (row, index) => string | - | Stable row ids, so selection survives data changes. |
State
Each piece of state (sorting, columnFilters, globalFilter, pagination, rowSelection, columnVisibility) has one owner:
- The table. This is the default. Read state from
table.stateand change it with methods such astable.setSortingorcolumn.toggleVisibility. - The table, with a starting value. Pass
initialState, for example{ pagination: { pageIndex: 0, pageSize: 25 } }. - You. Pass the value in
stateand its matchingon[Slice]Changecallback.
const [sorting, setSorting] = useState<SortingState>([{ id: 'name', desc: false }])
const table = useDataTable({
columns,
data,
state: { sorting },
onSortingChange: setSorting,
})DataTable
| Prop | Type | Default | Description |
|---|---|---|---|
| table | DataTableInstance<TData> | Required | The instance returned by useDataTable. |
| isLoading | boolean | false | Shows skeleton rows instead of data. |
| isFetching | boolean | false | Shows a spinner over the current rows. |
| emptyState | ReactNode | - | Replaces the default "No results." message. |
| onRowClick | (row: DataTableRow<TData>) => void | - | Makes rows clickable and focusable. Clicks on buttons, links and checkboxes are ignored. |
| skeletonRowCount | number | 5 | Number of skeleton rows while loading. |
DataTableToolbar
| Prop | Type | Default | Description |
|---|---|---|---|
| table | DataTableInstance<TData> | Required | The instance returned by useDataTable. |
| showSearch | boolean | true | Shows the global search input. |
| showViewOptions | boolean | true | Shows the column visibility menu. |
| children | ReactNode | - | Extra filters or actions, placed after the search input. |
The reset button appears while a search or column filter is active. It clears both.
<DataTableToolbar table={table}>
<Button size='sm' variant='outline' onClick={exportSelection}>
Export
</Button>
</DataTableToolbar>DataTablePagination
| Prop | Type | Default | Description |
|---|---|---|---|
| table | DataTableInstance<TData> | Required | The instance returned by useDataTable. |
| pageSizeOptions | number[] | [10, 20, 30, 40, 50] | Choices in the rows-per-page menu. |
| showSelectedCount | boolean | true with a selection column | Shows how many rows are selected. |
DataTableColumnHeader
| Prop | Type | Default | Description |
|---|---|---|---|
| column | DataTableColumn<TData> | Required | The column from the header context. |
| title | string | meta.label ?? column.id | Text shown in the header. |
The header shows only the controls a column supports. Turn them off per column with enableSorting: false or enableHiding: false. A column that can't sort, hide or filter renders as plain text.
Column meta
The components read these fields from each column's meta:
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | - | Readable column name for the header and the view options menu. |
| filter | FilterConfig | - | Adds a filter popover to the column header. |
DataTableColumnMeta is an interface. Extend it with declaration merging to add your own fields.
Filters
Set meta.filter on a column to add a filter popover to its header. useDataTable gives the column the matching filter function. A filterFn you set on the column yourself takes precedence.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | FilterVariant | Required | text, select, multiSelect, numberRange, dateRange or custom. Picks the input and the matching logic. |
| title | string | - | Heading at the top of the filter popover. |
| description | string | - | Helper text under the title. |
| placeholder | string | - | Placeholder for text and select filters. |
| options | FilterOption[] | - | Choices for select and multiSelect: { label, value, icon? }. |
| debounceMs | number | 300 | Delay before a text filter applies. |
| component | ComponentType<FilterComponentProps> | - | The input for the custom variant. Receives value and onChange. |
| Variant | Input | Filter value | Matches when |
|---|---|---|---|
text | Debounced text input | string | The value contains the text, ignoring case |
select | Single select | string | number | The value equals the option |
multiSelect | Searchable checklist | (string | number)[] | The value, or any item of an array value, is selected |
numberRange | Min and max inputs | [min?, max?] | The value is within the range (inclusive) |
dateRange | Range calendar | { from?: Date, to?: Date } | The date is within the range, whole days inclusive |
custom | Your component | Anything | Your column's filterFn returns true |
A missing end of a number or date range leaves that side open.
Custom filters
A custom filter provides the input as component and the matching logic as filterFn:
columnHelper.accessor('age', {
header: ({ column }) => <DataTableColumnHeader column={column} />,
filterFn: (row, columnId, adultsOnly: boolean) =>
!adultsOnly || row.getValue<number>(columnId) >= 18,
meta: {
label: 'Age',
filter: {
variant: 'custom',
component: ({ value, onChange }) => (
<Checkbox
checked={value === true}
onCheckedChange={(checked) => onChange(checked === true || undefined)}
/>
),
},
},
})Call onChange(undefined) to remove the filter.
Row selection
Add createSelectionColumn<TData>() as the first column. It adds a checkbox for each row and a "select all" checkbox for the current page in the header.
const table = useDataTable({
columns,
data,
getRowId: (user) => user.id,
enableRowSelection: (row) => row.original.status === 'active',
})
const selectedUsers = table.getSelectedRowModel().rows.map((row) => row.original)Row clicks
<DataTable table={table} onRowClick={(row) => router.push(`/users/${row.original.id}`)} />A clickable row can take keyboard focus and responds to Enter and Space. Clicks on buttons, links, checkboxes and menu items inside the row don't count as row clicks.
Loading states
Pass isLoading for the first load and isFetching for background refreshes. isLoading shows skeleton rows, and isFetching shows a spinner over the rows you already have.
<DataTable table={table} isLoading={query.isPending} isFetching={query.isFetching} />Localization
Wrap your tables in DataTableLocaleProvider. Any string you don't override keeps its English default. Strings that include numbers are functions.
import { DataTableLocaleProvider } from '@/components/shadcntable/lib/locale'
;<DataTableLocaleProvider
locale={{
toolbar: { searchPlaceholder: 'Rechercher...', reset: 'Réinitialiser' },
pagination: {
rowsPerPage: 'Lignes par page',
pageOf: (page, pageCount) => `Page ${page} sur ${pageCount}`,
rowsSelected: (selected, total) =>
`${selected} sur ${total} ligne(s) sélectionnée(s)`,
},
}}
>
<UsersTable users={users} />
</DataTableLocaleProvider>For every key, see DataTableLocale in lib/locale.tsx.
Server-side data
Set manualPagination, manualSorting or manualFiltering, own that state, and fetch with it. The table then renders your data as given. See the server-side example.