Shadcn Data Table
Free shadcn/ui data table for React — a reusable
Installation
{
"registries": {
"@designrevision": "https://registry.designrevision.com/r/{name}.json"
}
}
Add to your existing components.json. Requires shadcn/ui v2.3+.
Packages
Props
columns*
= —
ColumnDef<TData, TValue>[]
TanStack column definitions (accessorKey, header, cell).
data*
= —
TData[]
The row data.
toolbar
= —
(table) => React.ReactNode
Optional toolbar rendered above the table; receives the table instance for search, faceted filters, and view options.
A reusable Data Table built on @tanstack/react-table over the Table primitive. <DataTable columns data /> wires up sorting, filtering, pagination, row selection, column visibility, and faceting; pass a toolbar render-prop for search/facets/view-options. Ships with DataTableColumnHeader, DataTablePagination, DataTableViewOptions, DataTableFacetedFilter, and DataTableRowActions helpers. The menu-driven pieces (column header, view options, row actions) use Dropdown Menu; faceted filters use Popover + Command.
"use client"
export { DataTableColumnHeader } from "./data-table-column-header"
export { DataTablePagination } from "./data-table-pagination"
export { DataTableViewOptions } from "./data-table-view-options"
export { DataTableFacetedFilter } from "./data-table-faceted-filter"
export { DataTableRowActions } from "./data-table-row-actions"
import * as React from "react"
import {
type ColumnDef,
type ColumnFiltersState,
type SortingState,
type Table as TanStackTable,
type VisibilityState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { DataTablePagination } from "./data-table-pagination"
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
/** Optional toolbar rendered above the table; receives the table instance. */
toolbar?: (table: TanStackTable<TData>) => React.ReactNode
}
export function DataTable<TData, TValue>({
columns,
data,
toolbar,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
)
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({})
const [rowSelection, setRowSelection] = React.useState({})
const table = useReactTable({
data,
columns,
state: { sorting, columnFilters, columnVisibility, rowSelection },
enableRowSelection: true,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
})
return (
<div className="space-y-4">
{toolbar?.(table)}
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<DataTablePagination table={table} />
</div>
)
}
"use client"
import {
ChevronLeftIcon,
ChevronRightIcon,
ChevronsLeftIcon,
ChevronsRightIcon,
} from "lucide-react"
import { type Table } from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
interface DataTablePaginationProps<TData> {
table: Table<TData>
pageSizeOptions?: number[]
}
export function DataTablePagination<TData>({
table,
pageSizeOptions = [5, 10, 20, 30],
}: DataTablePaginationProps<TData>) {
return (
<div className="flex flex-wrap items-center justify-between gap-4 px-2">
<div className="text-muted-foreground flex-1 text-sm whitespace-nowrap">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="flex items-center gap-4 lg:gap-8">
<div className="flex items-center gap-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => table.setPageSize(Number(value))}
>
<SelectTrigger className="h-8 w-[70px]">
<SelectValue placeholder={table.getState().pagination.pageSize} />
</SelectTrigger>
<SelectContent side="top">
{pageSizeOptions.map((size) => (
<SelectItem key={size} value={`${size}`}>
{size}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
className="hidden size-8 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
aria-label="Go to first page"
>
<ChevronsLeftIcon />
</Button>
<Button
variant="outline"
size="icon"
className="size-8"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
aria-label="Go to previous page"
>
<ChevronLeftIcon />
</Button>
<Button
variant="outline"
size="icon"
className="size-8"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
aria-label="Go to next page"
>
<ChevronRightIcon />
</Button>
<Button
variant="outline"
size="icon"
className="hidden size-8 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
aria-label="Go to last page"
>
<ChevronsRightIcon />
</Button>
</div>
</div>
</div>
)
}
"use client"
import {
ArrowDownIcon,
ArrowUpIcon,
ChevronsUpDownIcon,
EyeOffIcon,
} from "lucide-react"
import { type Column } from "@tanstack/react-table"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
interface DataTableColumnHeaderProps<TData, TValue>
extends React.HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>
title: string
}
export function DataTableColumnHeader<TData, TValue>({
column,
title,
className,
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>
}
return (
<div className={cn("flex items-center gap-2", className)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="data-[state=open]:bg-accent -ml-3 h-8"
>
<span>{title}</span>
{column.getIsSorted() === "desc" ? (
<ArrowDownIcon />
) : column.getIsSorted() === "asc" ? (
<ArrowUpIcon />
) : (
<ChevronsUpDownIcon />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUpIcon className="text-muted-foreground/70" />
Asc
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<ArrowDownIcon className="text-muted-foreground/70" />
Desc
</DropdownMenuItem>
{column.getCanHide() && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeOffIcon className="text-muted-foreground/70" />
Hide
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
"use client"
import { Settings2Icon } from "lucide-react"
import { type Table } from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
interface DataTableViewOptionsProps<TData> {
table: Table<TData>
}
export function DataTableViewOptions<TData>({
table,
}: DataTableViewOptionsProps<TData>) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="ml-auto h-8">
<Settings2Icon />
View
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[150px]">
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
<DropdownMenuSeparator />
{table
.getAllColumns()
.filter(
(column) =>
typeof column.accessorFn !== "undefined" && column.getCanHide()
)
.map((column) => (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}
"use client"
import * as React from "react"
import { CheckIcon, PlusCircleIcon } from "lucide-react"
import { type Column } from "@tanstack/react-table"
import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
interface FacetedFilterOption {
label: string
value: string
icon?: React.ComponentType<{ className?: string }>
}
interface DataTableFacetedFilterProps<TData, TValue> {
column?: Column<TData, TValue>
title?: string
options: FacetedFilterOption[]
}
export function DataTableFacetedFilter<TData, TValue>({
column,
title,
options,
}: DataTableFacetedFilterProps<TData, TValue>) {
const facets = column?.getFacetedUniqueValues()
const selectedValues = new Set(column?.getFilterValue() as string[])
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="h-8 border-dashed">
<PlusCircleIcon />
{title}
{selectedValues?.size > 0 && (
<>
<div className="bg-border mx-2 h-4 w-px" />
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal lg:hidden"
>
{selectedValues.size}
</Badge>
<div className="hidden gap-1 lg:flex">
{selectedValues.size > 2 ? (
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal"
>
{selectedValues.size} selected
</Badge>
) : (
options
.filter((option) => selectedValues.has(option.value))
.map((option) => (
<Badge
variant="secondary"
key={option.value}
className="rounded-sm px-1 font-normal"
>
{option.label}
</Badge>
))
)}
</div>
</>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
<Command>
<CommandInput placeholder={title} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{options.map((option) => {
const isSelected = selectedValues.has(option.value)
return (
<CommandItem
key={option.value}
onSelect={() => {
if (isSelected) {
selectedValues.delete(option.value)
} else {
selectedValues.add(option.value)
}
const filterValues = Array.from(selectedValues)
column?.setFilterValue(
filterValues.length ? filterValues : undefined
)
}}
>
<div
className={cn(
"flex size-4 items-center justify-center rounded-[4px] border",
isSelected
? "bg-primary border-primary text-primary-foreground"
: "border-input [&_svg]:invisible"
)}
>
<CheckIcon className="size-3.5" />
</div>
{option.icon && (
<option.icon className="text-muted-foreground size-4" />
)}
<span>{option.label}</span>
{facets?.get(option.value) && (
<span className="ml-auto flex size-4 items-center justify-center font-mono text-xs">
{facets.get(option.value)}
</span>
)}
</CommandItem>
)
})}
</CommandGroup>
{selectedValues.size > 0 && (
<>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => column?.setFilterValue(undefined)}
className="justify-center text-center"
>
Clear filters
</CommandItem>
</CommandGroup>
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
"use client"
import { MoreHorizontalIcon } from "lucide-react"
import { type Row } from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
interface DataTableRowActionsProps<TData> {
row: Row<TData>
onEdit?: (row: Row<TData>) => void
onDelete?: (row: Row<TData>) => void
}
export function DataTableRowActions<TData>({
row,
onEdit,
onDelete,
}: DataTableRowActionsProps<TData>) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="data-[state=open]:bg-muted size-8"
>
<MoreHorizontalIcon />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[160px]">
<DropdownMenuItem onClick={() => onEdit?.(row)}>Edit</DropdownMenuItem>
<DropdownMenuItem>Make a copy</DropdownMenuItem>
<DropdownMenuItem>Favorite</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onClick={() => onDelete?.(row)}>
Delete
<DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
Examples
Data Table
The reusable `
Packages
Props
No props documented yet.
"use client"
import { type ColumnDef } from "@tanstack/react-table"
import { DataTable, DataTableColumnHeader } from "@/components/ui/data-table"
type Payment = {
id: string
amount: number
status: "pending" | "processing" | "success" | "failed"
email: string
}
const data: Payment[] = [
{ id: "1", amount: 316, status: "success", email: "[email protected]" },
{ id: "2", amount: 242, status: "success", email: "[email protected]" },
{ id: "3", amount: 837, status: "processing", email: "[email protected]" },
{ id: "4", amount: 874, status: "success", email: "[email protected]" },
{ id: "5", amount: 721, status: "failed", email: "[email protected]" },
{ id: "6", amount: 459, status: "pending", email: "[email protected]" },
{ id: "7", amount: 633, status: "processing", email: "[email protected]" },
{ id: "8", amount: 192, status: "success", email: "[email protected]" },
{ id: "9", amount: 548, status: "failed", email: "[email protected]" },
{ id: "10", amount: 387, status: "pending", email: "[email protected]" },
{ id: "11", amount: 905, status: "success", email: "[email protected]" },
{ id: "12", amount: 264, status: "processing", email: "[email protected]" },
]
const columns: ColumnDef<Payment>[] = [
{
accessorKey: "status",
header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
cell: ({ row }) => <span className="capitalize">{row.getValue("status")}</span>,
},
{
accessorKey: "email",
header: ({ column }) => <DataTableColumnHeader column={column} title="Email" />,
},
{
accessorKey: "amount",
header: () => <div className="text-right">Amount</div>,
cell: ({ row }) => {
const amount = parseFloat(row.getValue("amount"))
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount)
return <div className="text-right font-medium tabular-nums">{formatted}</div>
},
},
]
export default function DataTableDemo01() {
return <DataTable columns={columns} data={data} />
}
Row selection
A [Checkbox](/components/checkbox) column with an indeterminate select-all, wired through TanStack's row-selection state and the pagination count.
Packages
Props
No props documented yet.
"use client"
import { type ColumnDef } from "@tanstack/react-table"
import { Checkbox } from "@/components/ui/checkbox"
import { DataTable, DataTableColumnHeader } from "@/components/ui/data-table"
type User = {
id: string
name: string
email: string
role: "owner" | "member" | "viewer"
}
const data: User[] = [
{ id: "1", name: "Ada Lovelace", email: "[email protected]", role: "owner" },
{ id: "2", name: "Alan Turing", email: "[email protected]", role: "member" },
{ id: "3", name: "Grace Hopper", email: "[email protected]", role: "member" },
{ id: "4", name: "Katherine Johnson", email: "[email protected]", role: "viewer" },
{ id: "5", name: "Margaret Hamilton", email: "[email protected]", role: "member" },
{ id: "6", name: "Hedy Lamarr", email: "[email protected]", role: "viewer" },
{ id: "7", name: "Dorothy Vaughan", email: "[email protected]", role: "member" },
{ id: "8", name: "Radia Perlman", email: "[email protected]", role: "owner" },
{ id: "9", name: "Barbara Liskov", email: "[email protected]", role: "member" },
{ id: "10", name: "Frances Allen", email: "[email protected]", role: "viewer" },
{ id: "11", name: "Shafi Goldwasser", email: "[email protected]", role: "member" },
{ id: "12", name: "Carol Shaw", email: "[email protected]", role: "viewer" },
]
const columns: ColumnDef<User>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "name",
header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
cell: ({ row }) => <span className="font-medium">{row.getValue("name")}</span>,
},
{
accessorKey: "email",
header: ({ column }) => <DataTableColumnHeader column={column} title="Email" />,
},
{
accessorKey: "role",
header: "Role",
cell: ({ row }) => <span className="capitalize">{row.getValue("role")}</span>,
},
]
export default function DataTableSelection01() {
return <DataTable columns={columns} data={data} />
}
Filter toolbar
A toolbar with a column-search input and a reset button — column filtering via `getFilteredRowModel`.
Packages
Props
No props documented yet.
"use client"
import { type ColumnDef } from "@tanstack/react-table"
import { XIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { DataTable, DataTableColumnHeader } from "@/components/ui/data-table"
import { Input } from "@/components/ui/input"
type Payment = {
id: string
amount: number
status: "pending" | "processing" | "success" | "failed"
email: string
}
const data: Payment[] = [
{ id: "1", amount: 316, status: "success", email: "[email protected]" },
{ id: "2", amount: 242, status: "success", email: "[email protected]" },
{ id: "3", amount: 837, status: "processing", email: "[email protected]" },
{ id: "4", amount: 874, status: "success", email: "[email protected]" },
{ id: "5", amount: 721, status: "failed", email: "[email protected]" },
{ id: "6", amount: 459, status: "pending", email: "[email protected]" },
{ id: "7", amount: 633, status: "processing", email: "[email protected]" },
{ id: "8", amount: 192, status: "success", email: "[email protected]" },
{ id: "9", amount: 548, status: "failed", email: "[email protected]" },
{ id: "10", amount: 387, status: "pending", email: "[email protected]" },
{ id: "11", amount: 905, status: "success", email: "[email protected]" },
{ id: "12", amount: 264, status: "processing", email: "[email protected]" },
]
const columns: ColumnDef<Payment>[] = [
{
accessorKey: "status",
header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
cell: ({ row }) => <span className="capitalize">{row.getValue("status")}</span>,
},
{
accessorKey: "email",
header: ({ column }) => <DataTableColumnHeader column={column} title="Email" />,
},
{
accessorKey: "amount",
header: () => <div className="text-right">Amount</div>,
cell: ({ row }) => {
const amount = parseFloat(row.getValue("amount"))
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount)
return <div className="text-right font-medium tabular-nums">{formatted}</div>
},
},
]
export default function DataTableFilter01() {
return (
<DataTable
columns={columns}
data={data}
toolbar={(table) => {
const isFiltered = table.getState().columnFilters.length > 0
return (
<div className="flex items-center gap-2">
<Input
placeholder="Filter emails…"
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("email")?.setFilterValue(event.target.value)
}
className="h-8 w-[180px] lg:w-[250px]"
/>
{isFiltered && (
<Button
variant="ghost"
size="sm"
className="h-8 px-2 lg:px-3"
onClick={() => table.resetColumnFilters()}
>
Reset
<XIcon />
</Button>
)}
</div>
)
}}
/>
)
}
Column visibility
A View-options menu to show and hide columns — built on [Dropdown Menu](/components/dropdown-menu) checkbox items.
Packages
Props
No props documented yet.
"use client"
import { type ColumnDef } from "@tanstack/react-table"
import {
DataTable,
DataTableColumnHeader,
DataTableViewOptions,
} from "@/components/ui/data-table"
type User = {
id: string
name: string
email: string
role: "owner" | "member" | "viewer"
team: string
}
const data: User[] = [
{ id: "1", name: "Ada Lovelace", email: "[email protected]", role: "owner", team: "Engineering" },
{ id: "2", name: "Alan Turing", email: "[email protected]", role: "member", team: "Research" },
{ id: "3", name: "Grace Hopper", email: "[email protected]", role: "member", team: "Engineering" },
{ id: "4", name: "Katherine Johnson", email: "[email protected]", role: "viewer", team: "Research" },
{ id: "5", name: "Margaret Hamilton", email: "[email protected]", role: "member", team: "Apollo" },
{ id: "6", name: "Hedy Lamarr", email: "[email protected]", role: "viewer", team: "Comms" },
{ id: "7", name: "Dorothy Vaughan", email: "[email protected]", role: "member", team: "Research" },
{ id: "8", name: "Radia Perlman", email: "[email protected]", role: "owner", team: "Networking" },
{ id: "9", name: "Barbara Liskov", email: "[email protected]", role: "member", team: "Engineering" },
{ id: "10", name: "Frances Allen", email: "[email protected]", role: "viewer", team: "Compilers" },
{ id: "11", name: "Shafi Goldwasser", email: "[email protected]", role: "member", team: "Security" },
{ id: "12", name: "Carol Shaw", email: "[email protected]", role: "viewer", team: "Games" },
]
const columns: ColumnDef<User>[] = [
{
accessorKey: "name",
header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
cell: ({ row }) => <span className="font-medium">{row.getValue("name")}</span>,
},
{
accessorKey: "email",
header: ({ column }) => <DataTableColumnHeader column={column} title="Email" />,
},
{
accessorKey: "role",
header: ({ column }) => <DataTableColumnHeader column={column} title="Role" />,
cell: ({ row }) => <span className="capitalize">{row.getValue("role")}</span>,
},
{
accessorKey: "team",
header: ({ column }) => <DataTableColumnHeader column={column} title="Team" />,
},
]
export default function DataTableVisibility01() {
return (
<DataTable
columns={columns}
data={data}
toolbar={(table) => (
<div className="flex items-center justify-end">
<DataTableViewOptions table={table} />
</div>
)}
/>
)
}
Faceted filters
A multi-select facet ([Popover](/components/popover) + [Command](/components/command)) with live option counts and selected-value badges — the classic data-table filter.
Packages
Props
No props documented yet.
"use client"
import { type ColumnDef } from "@tanstack/react-table"
import {
CircleCheckIcon,
CircleDashedIcon,
CircleDotIcon,
CircleIcon,
CircleXIcon,
} from "lucide-react"
import {
DataTable,
DataTableColumnHeader,
DataTableFacetedFilter,
} from "@/components/ui/data-table"
const statuses = [
{ value: "backlog", label: "Backlog", icon: CircleDashedIcon },
{ value: "todo", label: "Todo", icon: CircleIcon },
{ value: "in progress", label: "In Progress", icon: CircleDotIcon },
{ value: "done", label: "Done", icon: CircleCheckIcon },
{ value: "canceled", label: "Canceled", icon: CircleXIcon },
]
type Task = { id: string; title: string; status: string }
const data: Task[] = [
{ id: "TASK-8782", title: "Refactor the auth middleware", status: "in progress" },
{ id: "TASK-7878", title: "Add dark mode toggle", status: "backlog" },
{ id: "TASK-7839", title: "Fix the pagination off-by-one", status: "done" },
{ id: "TASK-5562", title: "Write the onboarding docs", status: "todo" },
{ id: "TASK-8686", title: "Migrate to the new API", status: "canceled" },
{ id: "TASK-1280", title: "Compress hero images", status: "done" },
{ id: "TASK-7262", title: "Audit the color tokens", status: "in progress" },
{ id: "TASK-1138", title: "Set up the staging env", status: "todo" },
{ id: "TASK-7184", title: "Remove the legacy flags", status: "backlog" },
{ id: "TASK-5160", title: "Add e2e tests for checkout", status: "todo" },
{ id: "TASK-5618", title: "Tune the cache headers", status: "done" },
{ id: "TASK-6699", title: "Draft the Q3 roadmap", status: "in progress" },
]
const columns: ColumnDef<Task>[] = [
{
accessorKey: "id",
header: ({ column }) => <DataTableColumnHeader column={column} title="Task" />,
cell: ({ row }) => (
<span className="text-muted-foreground font-mono text-xs">
{row.getValue("id")}
</span>
),
},
{
accessorKey: "title",
header: ({ column }) => <DataTableColumnHeader column={column} title="Title" />,
cell: ({ row }) => <span className="font-medium">{row.getValue("title")}</span>,
},
{
accessorKey: "status",
header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
cell: ({ row }) => {
const status = statuses.find((s) => s.value === row.getValue("status"))
if (!status) return null
return (
<div className="flex items-center gap-2">
<status.icon className="text-muted-foreground size-4" />
<span>{status.label}</span>
</div>
)
},
filterFn: (row, id, value) => (value as string[]).includes(row.getValue(id)),
},
]
export default function DataTableFaceted01() {
return (
<DataTable
columns={columns}
data={data}
toolbar={(table) => (
<div className="flex items-center gap-2">
<DataTableFacetedFilter
column={table.getColumn("status")}
title="Status"
options={statuses}
/>
</div>
)}
/>
)
}
Row actions
A per-row "⋯" actions menu ([Dropdown Menu](/components/dropdown-menu)) with edit, copy, and a destructive delete.
Packages
Props
No props documented yet.
"use client"
import { type ColumnDef } from "@tanstack/react-table"
import { Badge } from "@/components/ui/badge"
import {
DataTable,
DataTableColumnHeader,
DataTableRowActions,
} from "@/components/ui/data-table"
type Task = {
id: string
title: string
type: "Bug" | "Feature" | "Documentation"
status: "todo" | "in progress" | "done"
}
const data: Task[] = [
{ id: "TASK-8782", title: "Refactor the auth middleware", type: "Bug", status: "in progress" },
{ id: "TASK-7878", title: "Add dark mode toggle", type: "Feature", status: "todo" },
{ id: "TASK-7839", title: "Fix the pagination off-by-one", type: "Bug", status: "done" },
{ id: "TASK-5562", title: "Write the onboarding docs", type: "Documentation", status: "todo" },
{ id: "TASK-1280", title: "Compress hero images", type: "Feature", status: "done" },
{ id: "TASK-7262", title: "Audit the color tokens", type: "Documentation", status: "in progress" },
{ id: "TASK-1138", title: "Set up the staging env", type: "Feature", status: "todo" },
{ id: "TASK-5160", title: "Add e2e tests for checkout", type: "Bug", status: "todo" },
{ id: "TASK-5618", title: "Tune the cache headers", type: "Feature", status: "done" },
{ id: "TASK-6699", title: "Draft the Q3 roadmap", type: "Documentation", status: "in progress" },
{ id: "TASK-2342", title: "Patch the XSS in comments", type: "Bug", status: "todo" },
{ id: "TASK-9081", title: "Localize the checkout flow", type: "Feature", status: "todo" },
]
const columns: ColumnDef<Task>[] = [
{
accessorKey: "id",
header: ({ column }) => <DataTableColumnHeader column={column} title="Task" />,
cell: ({ row }) => (
<span className="text-muted-foreground font-mono text-xs">
{row.getValue("id")}
</span>
),
},
{
accessorKey: "title",
header: ({ column }) => <DataTableColumnHeader column={column} title="Title" />,
cell: ({ row }) => (
<div className="flex items-center gap-2">
<Badge variant="outline">{row.original.type}</Badge>
<span className="font-medium">{row.getValue("title")}</span>
</div>
),
},
{
accessorKey: "status",
header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
cell: ({ row }) => <span className="capitalize">{row.getValue("status")}</span>,
},
{
id: "actions",
cell: ({ row }) => (
<div className="text-right">
<DataTableRowActions row={row} />
</div>
),
enableSorting: false,
enableHiding: false,
},
]
export default function DataTableRowActions01() {
return <DataTable columns={columns} data={data} />
}
Full tasks table
The complete showcase — selection, sortable headers, status & priority faceted filters, global search, column visibility, row actions, and pagination together.
Packages
Props
No props documented yet.
"use client"
import { type ColumnDef } from "@tanstack/react-table"
import {
ArrowDownIcon,
ArrowRightIcon,
ArrowUpIcon,
CircleCheckIcon,
CircleDashedIcon,
CircleDotIcon,
CircleIcon,
CircleXIcon,
XIcon,
} from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
DataTable,
DataTableColumnHeader,
DataTableFacetedFilter,
DataTableRowActions,
DataTableViewOptions,
} from "@/components/ui/data-table"
import { Input } from "@/components/ui/input"
const statuses = [
{ value: "backlog", label: "Backlog", icon: CircleDashedIcon },
{ value: "todo", label: "Todo", icon: CircleIcon },
{ value: "in progress", label: "In Progress", icon: CircleDotIcon },
{ value: "done", label: "Done", icon: CircleCheckIcon },
{ value: "canceled", label: "Canceled", icon: CircleXIcon },
]
const priorities = [
{ value: "low", label: "Low", icon: ArrowDownIcon },
{ value: "medium", label: "Medium", icon: ArrowRightIcon },
{ value: "high", label: "High", icon: ArrowUpIcon },
]
type Task = {
id: string
title: string
type: "Bug" | "Feature" | "Documentation"
status: string
priority: string
}
const data: Task[] = [
{ id: "TASK-8782", title: "Refactor the auth middleware", type: "Bug", status: "in progress", priority: "high" },
{ id: "TASK-7878", title: "Add dark mode toggle", type: "Feature", status: "backlog", priority: "medium" },
{ id: "TASK-7839", title: "Fix the pagination off-by-one", type: "Bug", status: "done", priority: "high" },
{ id: "TASK-5562", title: "Write the onboarding docs", type: "Documentation", status: "todo", priority: "low" },
{ id: "TASK-8686", title: "Migrate to the new API", type: "Feature", status: "canceled", priority: "medium" },
{ id: "TASK-1280", title: "Compress hero images", type: "Feature", status: "done", priority: "low" },
{ id: "TASK-7262", title: "Audit the color tokens", type: "Documentation", status: "in progress", priority: "medium" },
{ id: "TASK-1138", title: "Set up the staging env", type: "Feature", status: "todo", priority: "high" },
{ id: "TASK-7184", title: "Remove the legacy flags", type: "Bug", status: "backlog", priority: "low" },
{ id: "TASK-5160", title: "Add e2e tests for checkout", type: "Bug", status: "todo", priority: "high" },
{ id: "TASK-5618", title: "Tune the cache headers", type: "Feature", status: "done", priority: "medium" },
{ id: "TASK-6699", title: "Draft the Q3 roadmap", type: "Documentation", status: "in progress", priority: "medium" },
{ id: "TASK-2342", title: "Patch the XSS in comments", type: "Bug", status: "todo", priority: "high" },
{ id: "TASK-9081", title: "Localize the checkout flow", type: "Feature", status: "todo", priority: "low" },
{ id: "TASK-4123", title: "Add rate limiting to the API", type: "Feature", status: "backlog", priority: "medium" },
{ id: "TASK-3377", title: "Document the deploy runbook", type: "Documentation", status: "done", priority: "low" },
]
const columns: ColumnDef<Task>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "id",
header: ({ column }) => <DataTableColumnHeader column={column} title="Task" />,
cell: ({ row }) => (
<span className="text-muted-foreground font-mono text-xs">
{row.getValue("id")}
</span>
),
enableHiding: false,
},
{
accessorKey: "title",
header: ({ column }) => <DataTableColumnHeader column={column} title="Title" />,
cell: ({ row }) => (
<div className="flex items-center gap-2">
<Badge variant="outline">{row.original.type}</Badge>
<span className="max-w-[320px] truncate font-medium">
{row.getValue("title")}
</span>
</div>
),
},
{
accessorKey: "status",
header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
cell: ({ row }) => {
const status = statuses.find((s) => s.value === row.getValue("status"))
if (!status) return null
return (
<div className="flex items-center gap-2">
<status.icon className="text-muted-foreground size-4" />
<span>{status.label}</span>
</div>
)
},
filterFn: (row, id, value) => (value as string[]).includes(row.getValue(id)),
},
{
accessorKey: "priority",
header: ({ column }) => <DataTableColumnHeader column={column} title="Priority" />,
cell: ({ row }) => {
const priority = priorities.find((p) => p.value === row.getValue("priority"))
if (!priority) return null
return (
<div className="flex items-center gap-2">
<priority.icon className="text-muted-foreground size-4" />
<span>{priority.label}</span>
</div>
)
},
filterFn: (row, id, value) => (value as string[]).includes(row.getValue(id)),
},
{
id: "actions",
cell: ({ row }) => (
<div className="text-right">
<DataTableRowActions row={row} />
</div>
),
enableSorting: false,
enableHiding: false,
},
]
export default function DataTableFull01() {
return (
<DataTable
columns={columns}
data={data}
toolbar={(table) => {
const isFiltered = table.getState().columnFilters.length > 0
return (
<div className="flex flex-wrap items-center gap-2">
<Input
placeholder="Filter tasks…"
value={(table.getColumn("title")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("title")?.setFilterValue(event.target.value)
}
className="h-8 w-[150px] lg:w-[250px]"
/>
<DataTableFacetedFilter
column={table.getColumn("status")}
title="Status"
options={statuses}
/>
<DataTableFacetedFilter
column={table.getColumn("priority")}
title="Priority"
options={priorities}
/>
{isFiltered && (
<Button
variant="ghost"
size="sm"
className="h-8 px-2 lg:px-3"
onClick={() => table.resetColumnFilters()}
>
Reset
<XIcon />
</Button>
)}
<DataTableViewOptions table={table} />
</div>
)
}}
/>
)
}
The Shadcn Data Table is the Table with a brain — a reusable <DataTable> built on TanStack Table that adds sorting, filtering, faceted filters, column visibility, row selection, row actions, and pagination. shadcn ships this as a step-by-step guide; we package it as a drop-in component plus the standard helpers, with examples that build up from a basic table to the full "tasks" showcase.
Built on TanStack Table
The Data Table is the static Table primitive driven by @tanstack/react-table — a headless library that owns the data logic (the row models for sorting, filtering, pagination, faceting, selection, expansion) and leaves the markup to you. shadcn supplies the markup via the Table primitive; we wire the two together. Install it once:
npm install @tanstack/react-table
Table vs Data Table
Reach for the right one:
- Table — static, styled markup with no dependency. It already does sorting, selection, and pagination in plain React state for moderate data.
- Data Table — a column-driven table where one engine handles sorting, filtering, faceted filters, visibility, and pagination together. Worth it once the feature set grows.
The <DataTable> wrapper
Rather than hand-wiring useReactTable every time, pass your columns and data to the wrapper. It owns the state (sorting, filters, visibility, selection), the render loop, the empty state, and the pagination bar:
<DataTable columns={columns} data={data} />
Columns are TanStack ColumnDefs — accessorKey, a header, and an optional cell renderer. For anything above the table (search, filters, view options) pass a toolbar render-prop, which receives the live table instance:
<DataTable columns={columns} data={data} toolbar={(table) => <MyToolbar table={table} />} />
Sorting
Render a column's header with DataTableColumnHeader and it becomes a menu with Asc / Desc / Hide, backed by getSortedRowModel():
header: ({ column }) => <DataTableColumnHeader column={column} title="Email" />
The menu uses Dropdown Menu, so it's fully keyboard-navigable. The Data Table example has three sortable columns.
Filtering & faceted filters
A simple filter is an Input bound to a column (the Filter toolbar example). A faceted filter is the data-table classic — a multi-select dropdown (Popover + Command) with live counts and selected-value badges:
<DataTableFacetedFilter column={table.getColumn("status")} title="Status" options={statuses} />
Give the column a filterFn that checks array membership and the facet counts populate from getFacetedUniqueValues(). The Faceted filters example filters tasks by status.
Selection, visibility & row actions
- Row selection — add a checkbox column whose header drives
toggleAllPageRowsSelected(with an indeterminate state) and whose cell drivesrow.toggleSelected. The pagination bar reports "N of M selected". - Column visibility — drop
<DataTableViewOptions table={table} />in the toolbar for a Dropdown Menu of show/hide checkboxes. - Row actions — add an actions column rendering
<DataTableRowActions row={row} />— a "⋯" menu with edit, copy, and a destructive delete.
The Full tasks table example assembles all of these — selection, sortable headers, two faceted filters, global search, view options, row actions, and pagination — the layout everyone means by "shadcn data table".
Server-side data
The wrapper is client-side, which is ideal for hundreds to a few thousand rows. For large or server-paged datasets, TanStack supports manual mode: set manualSorting / manualFiltering / manualPagination, drive the state from your query, and fetch each page from your API instead of the client row models.
Accessibility & theming
The Data Table inherits the Table's semantic HTML, and the menu-driven pieces use Dropdown Menu's roving-focus keyboard semantics, so headers, actions, and view options are all keyboard-operable. Everything reads your design tokens — --border, --muted, --accent, --primary — so it follows your theme, including dark mode, with no overrides.