DataTable
A powerful, fully-featured data table component built on TanStack Table
Overview
The DataTable component provides a feature-rich table with sorting, filtering, pagination, row selection, and more. It's built on top of TanStack Table and styled with shadcn/ui components.
Installation
pnpm dlx shadcn@latest add https://shadcntable.com/r/data-table.jsonBasic Usage
import { type ColumnDef } from '@tanstack/react-table'
import { DataTable } from '@/components/shadcntable/data-table'
import { DataTableColumnHeader } from '@/components/shadcntable/data-table-column-header'
type User = {
id: string
name: string
email: string
}
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: ({ column }) => <DataTableColumnHeader column={column} title='Name' />,
},
{
accessorKey: 'email',
header: ({ column }) => <DataTableColumnHeader column={column} title='Email' />,
},
]
const data: User[] = [
{ id: '1', name: 'John Doe', email: 'john@example.com' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' },
]
export function MyTable() {
return <DataTable columns={columns} data={data} />
}DataTable Props
The main DataTable component accepts the following props:
| Prop | Type | Default | Description |
|---|---|---|---|
| columns | ColumnDef<TData, TValue>[] | Required | Column definitions using TanStack Table's ColumnDef |
| data | TData[] | Required | Array of data to display |
| emptyState | React.ReactNode | - | Custom empty state when no data |
| isLoading | boolean | false | Shows loading skeleton when true |
| locale | Partial<DataTableLocale> | - | Override default text strings for internationalization |
| onRowClick | (row: TData) => void | - | Callback when a row is clicked |
| pagination | DataTablePaginationConfig | - | Pagination configuration |
| rowSelection | DataTableRowSelectionConfig | - | Row selection configuration |
| toolbar | DataTableToolbarConfig | - | Toolbar configuration |
Pagination
Configure pagination using the pagination prop:
<DataTable
columns={columns}
data={data}
pagination={{
enabled: true,
pageSize: 10,
pageSizeOptions: [10, 25, 50, 100],
}}
/>DataTablePaginationConfig
| Prop | Type | Default | Description |
|---|---|---|---|
| enabled | boolean | true | Enable/disable pagination |
| pageSize | number | 10 | Initial page size |
| pageSizeOptions | number[] | [10, 25, 50] | Available page size options |
Row Selection
Enable row selection with checkboxes:
<DataTable
columns={columns}
data={data}
rowSelection={{
enableRowSelection: true,
onRowSelectionChange: (selectedRows) => {
console.log('Selected:', selectedRows)
},
}}
/>You can also conditionally enable selection per row:
<DataTable
columns={columns}
data={data}
rowSelection={{
enableRowSelection: (row) => row.original.status !== 'locked',
onRowSelectionChange: (selectedRows) => {
console.log('Selected:', selectedRows)
},
}}
/>DataTableRowSelectionConfig
| Prop | Type | Default | Description |
|---|---|---|---|
| enableRowSelection | boolean | ((row: Row<TData>) => boolean) | - | Enable selection globally or per-row |
| onRowSelectionChange | (selectedRows: TData[]) => void | - | Callback when selection changes |
Toolbar
Configure the toolbar with search and view options:
<DataTable
columns={columns}
data={data}
toolbar={{
search: true,
viewOptions: true,
}}
/>DataTableToolbarConfig
| Prop | Type | Default | Description |
|---|---|---|---|
| search | boolean | true | Show global search input |
| viewOptions | boolean | true | Show column visibility toggle |
Column Header
Use DataTableColumnHeader for sortable column headers with filtering:
import { DataTableColumnHeader } from '@/components/shadcntable/data-table-column-header'
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: ({ column }) => <DataTableColumnHeader column={column} title='Name' />,
},
]The column header provides:
- Sorting - Click to sort ascending/descending
- Column visibility - Option to hide the column
- Filtering - When
filterConfigis set in column meta
Column Filters
Add filters to columns using the meta.filterConfig property:
Text Filter
{
accessorKey: 'name',
header: ({ column }) => <DataTableColumnHeader column={column} title='Name' />,
meta: {
filterConfig: {
title: 'Filter by name',
description: 'Search for users by name',
variant: 'text',
placeholder: 'Enter name...',
debounceMs: 300,
},
},
}Select Filter
{
accessorKey: 'status',
header: ({ column }) => <DataTableColumnHeader column={column} title='Status' />,
meta: {
filterConfig: {
title: 'Filter by status',
variant: 'select',
placeholder: 'Select status...',
options: [
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' },
{ label: 'Pending', value: 'pending' },
],
},
},
}Number Range Filter
{
accessorKey: 'age',
header: ({ column }) => <DataTableColumnHeader column={column} title='Age' />,
meta: {
filterConfig: {
title: 'Filter by age',
variant: 'number-range',
placeholder: 'Min - Max',
},
},
}Date Range Filter
{
accessorKey: 'createdAt',
header: ({ column }) => <DataTableColumnHeader column={column} title='Created' />,
meta: {
filterConfig: {
title: 'Filter by date',
variant: 'date-range',
placeholder: 'Select date range...',
},
},
}Custom Filter
{
accessorKey: 'age',
header: ({ column }) => <DataTableColumnHeader column={column} title='Age' />,
meta: {
filterConfig: {
variant: 'custom',
title: 'Age Filter',
component: ({ value, onChange }) => {
const isChecked = value === true
return (
<div className='flex items-center gap-2'>
<Checkbox
id='adults-only'
checked={isChecked}
onCheckedChange={(checked) => {
onChange(checked === true)
}}
/>
<Label htmlFor='adults-only' className='text-sm font-normal cursor-pointer'>
Adults only (18+)
</Label>
</div>
)
},
},
},
filterFn: (row, columnId, filterValue) => {
if (filterValue !== true) return true
const age = row.getValue<number>(columnId)
return age >= 18
},
}FilterConfig Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | FilterVariant | Required | Type of filter (text, select, date-range, etc.) |
| title | string | - | Title shown in filter popover |
| description | string | - | Description shown in filter popover |
| placeholder | string | - | Placeholder text |
| options | Array<{label, value}> | - | Options for select filters |
| debounceMs | number | - | Debounce delay in milliseconds |
| caseSensitive | boolean | false | Case-sensitive text matching |
| component | React.ComponentType | - | Custom filter component |
Filter Variants
| Prop | Type | Default | Description |
|---|---|---|---|
| text | string | - | Free text input |
| select | string | - | Single select dropdown |
| multi-select | string | - | Multi-select dropdown |
| date-range | string | - | Date range picker |
| number-range | string | - | Min/max number inputs |
| custom | string | - | Custom filter component |
Loading State
Show a loading skeleton while fetching data:
const [isLoading, setIsLoading] = useState(true)
<DataTable
columns={columns}
data={data}
isLoading={isLoading}
/>Custom Empty State
Customize the empty state when there's no data:
<DataTable
columns={columns}
data={[]}
emptyState={
<div className='text-center py-10'>
<p className='text-muted-foreground'>No users found</p>
<Button onClick={() => refetch()}>Refresh</Button>
</div>
}
/>Internationalization
Override all text strings in the DataTable using the locale prop. You can override individual strings or provide a complete locale object:
<DataTable
columns={columns}
data={data}
locale={{
body: {
noResults: 'Aucun résultat',
},
pagination: {
rowsSelected: 'ligne(s) sélectionnée(s).',
rowsPerPage: 'Lignes par page',
page: 'Page',
of: 'sur',
goToFirstPage: 'Aller à la première page',
goToPreviousPage: 'Aller à la page précédente',
goToNextPage: 'Aller à la page suivante',
goToLastPage: 'Aller à la dernière page',
},
toolbar: {
searchPlaceholder: 'Rechercher...',
},
viewOptions: {
view: 'Vue',
toggleColumns: 'Basculer les colonnes',
},
rowSelection: {
selectAll: 'Tout sélectionner',
selectRow: 'Sélectionner la ligne',
},
columnHeader: {
sortAscending: 'Trier par ordre croissant',
sortDescending: 'Trier par ordre décroissant',
clearSorting: 'Effacer le tri',
hideColumn: 'Masquer la colonne',
clearFilter: 'Effacer le filtre',
sortMenuLabel: 'Basculer les options de tri',
filterMenuLabel: 'Basculer les options de filtre',
},
filters: {
multiSelect: {
search: 'Rechercher...',
noResults: 'Aucun résultat trouvé.',
},
numberRange: {
min: 'Min',
max: 'Max',
},
},
}}
/>You can also override only specific strings:
<DataTable
columns={columns}
data={data}
locale={{
body: {
noResults: 'No data available',
},
pagination: {
rowsPerPage: 'Items per page',
},
}}
/>Locale Structure
The locale prop accepts a Partial<DataTableLocale> object with the following structure:
body.noResults- Empty state messagepagination.rowsSelected- Row selection count textpagination.rowsPerPage- "Rows per page" labelpagination.page- "Page" labelpagination.of- "of" separatorpagination.goToFirstPage- Screen reader text for first page buttonpagination.goToPreviousPage- Screen reader text for previous page buttonpagination.goToNextPage- Screen reader text for next page buttonpagination.goToLastPage- Screen reader text for last page buttontoolbar.searchPlaceholder- Default search input placeholderviewOptions.view- View options button labelviewOptions.toggleColumns- Toggle columns menu labelrowSelection.selectAll- Select all checkbox aria-labelrowSelection.selectRow- Select row checkbox aria-labelcolumnHeader.sortAscending- Sort ascending menu itemcolumnHeader.sortDescending- Sort descending menu itemcolumnHeader.clearSorting- Clear sorting menu itemcolumnHeader.hideColumn- Hide column menu itemcolumnHeader.clearFilter- Clear filter button textcolumnHeader.sortMenuLabel- Sort menu button aria-labelcolumnHeader.filterMenuLabel- Filter menu button aria-labelfilters.multiSelect.search- Multi-select filter search placeholderfilters.multiSelect.noResults- Multi-select filter no results messagefilters.numberRange.min- Number range filter min placeholderfilters.numberRange.max- Number range filter max placeholder
Row Click Handler
Handle row clicks:
<DataTable
columns={columns}
data={data}
onRowClick={(user) => {
router.push(`/users/${user.id}`)
}}
/>Custom Cell Rendering
Customize how cells are rendered:
const columns: ColumnDef<User>[] = [
{
accessorKey: 'status',
header: ({ column }) => <DataTableColumnHeader column={column} title='Status' />,
cell: ({ row }) => {
const status = row.getValue('status') as string
return (
<Badge variant={status === 'active' ? 'default' : 'secondary'}>{status}</Badge>
)
},
},
{
accessorKey: 'createdAt',
header: ({ column }) => <DataTableColumnHeader column={column} title='Created' />,
cell: ({ row }) => {
const date = new Date(row.getValue('createdAt'))
return date.toLocaleDateString()
},
},
]Actions Column
Add an actions column with a dropdown menu:
import { MoreHorizontal } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
const columns: ColumnDef<User>[] = [
// ... other columns
{
id: 'actions',
header: ({ column }) => <DataTableColumnHeader column={column} title='Actions' />,
cell: ({ row }) => {
const user = row.original
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='ghost' className='h-8 w-8 p-0'>
<MoreHorizontal className='h-4 w-4' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(user.id)}>
Copy ID
</DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push(`/users/${user.id}`)}>
View details
</DropdownMenuItem>
<DropdownMenuItem onClick={() => deleteUser(user.id)}>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
},
},
]Complete Example
Here's a complete example with all features:
'use client'
import { type ColumnDef } from '@tanstack/react-table'
import { MoreHorizontal } from 'lucide-react'
import { DataTable } from '@/components/shadcntable/data-table'
import { DataTableColumnHeader } from '@/components/shadcntable/data-table-column-header'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
type User = {
id: string
name: string
email: string
status: 'active' | 'inactive'
role: string
}
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: ({ column }) => <DataTableColumnHeader column={column} title='Name' />,
meta: {
filterConfig: {
variant: 'text',
placeholder: 'Filter names...',
},
},
},
{
accessorKey: 'email',
header: ({ column }) => <DataTableColumnHeader column={column} title='Email' />,
},
{
accessorKey: 'status',
header: ({ column }) => <DataTableColumnHeader column={column} title='Status' />,
cell: ({ row }) => {
const status = row.getValue('status') as string
return (
<Badge variant={status === 'active' ? 'default' : 'secondary'}>{status}</Badge>
)
},
meta: {
filterConfig: {
variant: 'select',
options: [
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' },
],
},
},
},
{
accessorKey: 'role',
header: ({ column }) => <DataTableColumnHeader column={column} title='Role' />,
},
{
id: 'actions',
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='ghost' size='icon'>
<MoreHorizontal className='h-4 w-4' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem>View</DropdownMenuItem>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
]
const users: User[] = [
{
id: '1',
name: 'John Doe',
email: 'john@example.com',
status: 'active',
role: 'Admin',
},
{
id: '2',
name: 'Jane Smith',
email: 'jane@example.com',
status: 'active',
role: 'User',
},
{
id: '3',
name: 'Bob Johnson',
email: 'bob@example.com',
status: 'inactive',
role: 'User',
},
]
export function UsersTable() {
return (
<DataTable
columns={columns}
data={users}
pagination={{
pageSize: 10,
pageSizeOptions: [10, 25, 50],
}}
rowSelection={{
enableRowSelection: true,
onRowSelectionChange: (rows) => console.log('Selected:', rows),
}}
toolbar={{
search: true,
viewOptions: true,
}}
/>
)
}On This Page
- Overview
- Installation
- Basic Usage
- DataTable Props
- Pagination
- DataTablePaginationConfig
- Row Selection
- DataTableRowSelectionConfig
- Toolbar
- DataTableToolbarConfig
- Column Header
- Column Filters
- Text Filter
- Select Filter
- Number Range Filter
- Date Range Filter
- Custom Filter
- FilterConfig Reference
- Filter Variants
- Loading State
- Custom Empty State
- Internationalization
- Locale Structure
- Row Click Handler
- Custom Cell Rendering
- Actions Column
- Complete Example