Data Table
React data table with sorting, filtering, pagination, and selection. Built with TanStack Table for powerful data management and user interactions.
Displaying data in tables? This component handles the complex stuff - sorting columns, filtering rows, selecting items, and paginating large datasets. Perfect for admin panels, dashboards, and anywhere you need to show structured data.
"use client"import * as React from "react"import { ColumnDef, ColumnFiltersState, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, SortingState, useReactTable, VisibilityState,} from "@tanstack/react-table"import { ArrowUpDown, ChevronDown, MoreHorizontal } from "lucide-react"import { Button } from "@/components/ui/button"import { Checkbox } from "@/components/ui/checkbox"import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger,} from "@/components/ui/dropdown-menu"import { Input } from "@/components/ui/input"import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@/components/ui/table"const data: Payment[] = [ { id: "m5gr84i9", amount: 316, status: "success", email: "[email protected]", }, { id: "3u1reuv4", amount: 242, status: "success", email: "[email protected]", }, { id: "derv1ws0", amount: 837, status: "processing", email: "[email protected]", }, { id: "5kma53ae", amount: 874, status: "success", email: "[email protected]", }, { id: "bhqecj4p", amount: 721, status: "failed", email: "[email protected]", },]export type Payment = { id: string amount: number status: "pending" | "processing" | "success" | "failed" email: string}export const columns: ColumnDef<Payment>[] = [ { 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: "status", header: "Status", cell: ({ row }) => ( <div className="capitalize">{row.getValue("status")}</div> ), }, { accessorKey: "email", header: ({ column }) => { return ( <Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} > Email <ArrowUpDown /> </Button> ) }, cell: ({ row }) => <div className="lowercase">{row.getValue("email")}</div>, }, { accessorKey: "amount", header: () => <div className="text-right">Amount</div>, cell: ({ row }) => { const amount = parseFloat(row.getValue("amount")) // Format the amount as a dollar amount const formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }).format(amount) return <div className="text-right font-medium">{formatted}</div> }, }, { id: "actions", enableHiding: false, cell: ({ row }) => { const payment = row.original return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" className="h-8 w-8 p-0"> <span className="sr-only">Open menu</span> <MoreHorizontal /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuLabel>Actions</DropdownMenuLabel> <DropdownMenuItem onClick={() => navigator.clipboard.writeText(payment.id)} > Copy payment ID </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem>View customer</DropdownMenuItem> <DropdownMenuItem>View payment details</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ) }, },]export default function DataTableDemo() { 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, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onColumnVisibilityChange: setColumnVisibility, onRowSelectionChange: setRowSelection, state: { sorting, columnFilters, columnVisibility, rowSelection, }, }) return ( <div className="w-full p-6"> <div className="flex items-center py-4"> <Input placeholder="Filter emails..." value={(table.getColumn("email")?.getFilterValue() as string) ?? ""} onChange={(event) => table.getColumn("email")?.setFilterValue(event.target.value) } className="max-w-sm" /> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="outline" className="ml-auto"> Columns <ChevronDown /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> {table .getAllColumns() .filter((column) => column.getCanHide()) .map((column) => { return ( <DropdownMenuCheckboxItem key={column.id} className="capitalize" checked={column.getIsVisible()} onCheckedChange={(value) => column.toggleVisibility(!!value) } > {column.id} </DropdownMenuCheckboxItem> ) })} </DropdownMenuContent> </DropdownMenu> </div> <div className="overflow-hidden rounded-md border"> <Table> <TableHeader> {table.getHeaderGroups().map((headerGroup) => ( <TableRow key={headerGroup.id}> {headerGroup.headers.map((header) => { return ( <TableHead key={header.id}> {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> <div className="flex items-center justify-end space-x-2 py-4"> <div className="text-muted-foreground flex-1 text-sm"> {table.getFilteredSelectedRowModel().rows.length} of{" "} {table.getFilteredRowModel().rows.length} row(s) selected. </div> <div className="space-x-2"> <Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()} > Previous </Button> <Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()} > Next </Button> </div> </div> </div> )}
import * as React from "react"import { Slot } from "@radix-ui/react-slot"import { cva, type VariantProps } from "class-variance-authority"import { cn } from "@/lib/utils"const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, })function Button({ className, variant, size, asChild = false, ...props}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) { const Comp = asChild ? Slot : "button" return ( <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} /> )}export { Button, buttonVariants }
Built on TanStack Table - the most powerful table library for React.
npx shadcn@latest add table
npm install @tanstack/react-table
Why this data table rocks
TanStack Table gives you enterprise-level features without the complexity:
- Click to sort any column - Users expect this, and it just works
- Search and filter data - Global search plus column-specific filters
- Select rows with checkboxes - Bulk actions made easy
- Handle huge datasets - Pagination and virtualization built-in
- Show/hide columns - Let users customize their view
- Fully accessible - Screen readers and keyboard navigation covered
- TypeScript native - Catch data errors at compile time
Data table patterns you need
Basic data display
Just show the data, clean and simple:
"use client"import * as React from "react"import { ColumnDef, flexRender, getCoreRowModel, useReactTable,} from "@tanstack/react-table"import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@/components/ui/table"const data: Payment[] = [ { id: "728ed52f", amount: 100, status: "pending", email: "[email protected]", }, { id: "489e1d42", amount: 125, status: "processing", email: "[email protected]", }, { id: "7c8f3a9b", amount: 75, status: "success", email: "[email protected]", },]export type Payment = { id: string amount: number status: "pending" | "processing" | "success" | "failed" email: string}export const columns: ColumnDef<Payment>[] = [ { accessorKey: "status", header: "Status", }, { accessorKey: "email", header: "Email", }, { accessorKey: "amount", header: "Amount", },]interface DataTableProps<TData, TValue> { columns: ColumnDef<TData, TValue>[] data: TData[]}function DataTable<TData, TValue>({ columns, data,}: DataTableProps<TData, TValue>) { const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), }) return ( <div className="overflow-hidden rounded-md border"> <Table> <TableHeader> {table.getHeaderGroups().map((headerGroup) => ( <TableRow key={headerGroup.id}> {headerGroup.headers.map((header) => { return ( <TableHead key={header.id}> {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> )}export default function DataTableSimple() { return ( <div className="w-full p-6"> <DataTable columns={columns} data={data} /> </div> )}
"use client"import * as React from "react"import { cn } from "@/lib/utils"function Table({ className, ...props }: React.ComponentProps<"table">) { return ( <div data-slot="table-container" className="relative w-full overflow-x-auto" > <table data-slot="table" className={cn("w-full caption-bottom text-sm", className)} {...props} /> </div> )}function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { return ( <thead data-slot="table-header" className={cn("[&_tr]:border-b", className)} {...props} /> )}function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { return ( <tbody data-slot="table-body" className={cn("[&_tr:last-child]:border-0", className)} {...props} /> )}function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { return ( <tfoot data-slot="table-footer" className={cn( "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className )} {...props} /> )}function TableRow({ className, ...props }: React.ComponentProps<"tr">) { return ( <tr data-slot="table-row" className={cn( "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", className )} {...props} /> )}function TableHead({ className, ...props }: React.ComponentProps<"th">) { return ( <th data-slot="table-head" className={cn( "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", className )} {...props} /> )}function TableCell({ className, ...props }: React.ComponentProps<"td">) { return ( <td data-slot="table-cell" className={cn( "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", className )} {...props} /> )}function TableCaption({ className, ...props}: React.ComponentProps<"caption">) { return ( <caption data-slot="table-caption" className={cn("text-muted-foreground mt-4 text-sm", className)} {...props} /> )}export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption,}
Sortable columns
Click any header to sort - exactly what users expect:
"use client"import * as React from "react"import { ColumnDef, SortingState, flexRender, getCoreRowModel, getSortedRowModel, useReactTable,} from "@tanstack/react-table"import { ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react"import { Button } from "@/components/ui/button"import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@/components/ui/table"const data: Payment[] = [ { id: "728ed52f", amount: 100, status: "pending", email: "[email protected]", }, { id: "489e1d42", amount: 125, status: "processing", email: "[email protected]", }, { id: "7c8f3a9b", amount: 75, status: "success", email: "[email protected]", }, { id: "a1b2c3d4", amount: 200, status: "failed", email: "[email protected]", }, { id: "e5f6g7h8", amount: 50, status: "success", email: "[email protected]", },]export type Payment = { id: string amount: number status: "pending" | "processing" | "success" | "failed" email: string}export const columns: ColumnDef<Payment>[] = [ { accessorKey: "status", header: ({ column }) => { return ( <Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} > Status {column.getIsSorted() === "asc" ? ( <ArrowUp className="ml-2 h-4 w-4" /> ) : column.getIsSorted() === "desc" ? ( <ArrowDown className="ml-2 h-4 w-4" /> ) : ( <ArrowUpDown className="ml-2 h-4 w-4" /> )} </Button> ) }, cell: ({ row }) => ( <div className="capitalize">{row.getValue("status")}</div> ), }, { accessorKey: "email", header: ({ column }) => { return ( <Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} > Email {column.getIsSorted() === "asc" ? ( <ArrowUp className="ml-2 h-4 w-4" /> ) : column.getIsSorted() === "desc" ? ( <ArrowDown className="ml-2 h-4 w-4" /> ) : ( <ArrowUpDown className="ml-2 h-4 w-4" /> )} </Button> ) }, cell: ({ row }) => <div className="lowercase">{row.getValue("email")}</div>, }, { accessorKey: "amount", header: ({ column }) => { return ( <Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} > Amount {column.getIsSorted() === "asc" ? ( <ArrowUp className="ml-2 h-4 w-4" /> ) : column.getIsSorted() === "desc" ? ( <ArrowDown className="ml-2 h-4 w-4" /> ) : ( <ArrowUpDown className="ml-2 h-4 w-4" /> )} </Button> ) }, 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">{formatted}</div> }, },]export default function DataTableSorting() { const [sorting, setSorting] = React.useState<SortingState>([]) const table = useReactTable({ data, columns, onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), state: { sorting, }, }) return ( <div className="w-full p-6"> <div className="overflow-hidden rounded-md border"> <Table> <TableHeader> {table.getHeaderGroups().map((headerGroup) => ( <TableRow key={headerGroup.id}> {headerGroup.headers.map((header) => { return ( <TableHead key={header.id}> {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> </div> )}
import * as React from "react"import { Slot } from "@radix-ui/react-slot"import { cva, type VariantProps } from "class-variance-authority"import { cn } from "@/lib/utils"const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, })function Button({ className, variant, size, asChild = false, ...props}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) { const Comp = asChild ? Slot : "button" return ( <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} /> )}export { Button, buttonVariants }
Search and filter
Users need to find specific rows quickly:
"use client"import * as React from "react"import { ColumnDef, ColumnFiltersState, flexRender, getCoreRowModel, getFilteredRowModel, useReactTable,} from "@tanstack/react-table"import { Input } from "@/components/ui/input"import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@/components/ui/table"const data: Payment[] = [ { id: "728ed52f", amount: 100, status: "pending", email: "[email protected]", }, { id: "489e1d42", amount: 125, status: "processing", email: "[email protected]", }, { id: "7c8f3a9b", amount: 75, status: "success", email: "[email protected]", }, { id: "a1b2c3d4", amount: 200, status: "failed", email: "[email protected]", }, { id: "e5f6g7h8", amount: 50, status: "success", email: "[email protected]", }, { id: "i9j0k1l2", amount: 300, status: "pending", email: "[email protected]", },]export type Payment = { id: string amount: number status: "pending" | "processing" | "success" | "failed" email: string}export const columns: ColumnDef<Payment>[] = [ { accessorKey: "status", header: "Status", cell: ({ row }) => ( <div className="capitalize">{row.getValue("status")}</div> ), }, { accessorKey: "email", header: "Email", cell: ({ row }) => <div className="lowercase">{row.getValue("email")}</div>, }, { 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">{formatted}</div> }, },]export default function DataTableFiltering() { const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( [] ) const table = useReactTable({ data, columns, onColumnFiltersChange: setColumnFilters, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), state: { columnFilters, }, }) return ( <div className="w-full p-6"> <div className="flex items-center py-4 space-x-4"> <Input placeholder="Filter emails..." value={(table.getColumn("email")?.getFilterValue() as string) ?? ""} onChange={(event) => table.getColumn("email")?.setFilterValue(event.target.value) } className="max-w-sm" /> <Input placeholder="Filter status..." value={(table.getColumn("status")?.getFilterValue() as string) ?? ""} onChange={(event) => table.getColumn("status")?.setFilterValue(event.target.value) } className="max-w-sm" /> </div> <div className="overflow-hidden rounded-md border"> <Table> <TableHeader> {table.getHeaderGroups().map((headerGroup) => ( <TableRow key={headerGroup.id}> {headerGroup.headers.map((header) => { return ( <TableHead key={header.id}> {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> <div className="flex items-center justify-end space-x-2 py-4"> <div className="text-muted-foreground text-sm"> {table.getFilteredRowModel().rows.length} of{" "} {data.length} row(s) showing. </div> </div> </div> )}
import * as React from "react"import { cn } from "@/lib/utils"function Input({ className, type, ...props }: React.ComponentProps<"input">) { return ( <input type={type} data-slot="input" className={cn( "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", className )} {...props} /> )}export { Input }
Break up large datasets
Pagination keeps things fast and manageable:
"use client"import * as React from "react"import { ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, useReactTable,} from "@tanstack/react-table"import { Button } from "@/components/ui/button"import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@/components/ui/table"// Generate more data for pagination democonst generateData = (count: number): Payment[] => { const statuses = ["pending", "processing", "success", "failed"] as const const domains = ["example.com", "gmail.com", "yahoo.com", "hotmail.com"] return Array.from({ length: count }, (_, i) => ({ id: `payment-${i + 1}`, amount: Math.floor(Math.random() * 1000) + 50, status: statuses[Math.floor(Math.random() * statuses.length)], email: `user${i + 1}@${domains[Math.floor(Math.random() * domains.length)]}`, }))}const data: Payment[] = generateData(50)export type Payment = { id: string amount: number status: "pending" | "processing" | "success" | "failed" email: string}export const columns: ColumnDef<Payment>[] = [ { accessorKey: "status", header: "Status", cell: ({ row }) => ( <div className="capitalize">{row.getValue("status")}</div> ), }, { accessorKey: "email", header: "Email", cell: ({ row }) => <div className="lowercase">{row.getValue("email")}</div>, }, { 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">{formatted}</div> }, },]export default function DataTablePagination() { const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), initialState: { pagination: { pageSize: 10, }, }, }) return ( <div className="w-full p-6"> <div className="overflow-hidden rounded-md border"> <Table> <TableHeader> {table.getHeaderGroups().map((headerGroup) => ( <TableRow key={headerGroup.id}> {headerGroup.headers.map((header) => { return ( <TableHead key={header.id}> {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> <div className="flex items-center justify-between space-x-2 py-4"> <div className="flex items-center space-x-2"> <p className="text-sm font-medium">Rows per page</p> <select value={table.getState().pagination.pageSize} onChange={(e) => { table.setPageSize(Number(e.target.value)) }} className="h-8 w-[70px] rounded border border-input bg-background px-3 py-1 text-sm" > {[10, 20, 30, 40, 50].map((pageSize) => ( <option key={pageSize} value={pageSize}> {pageSize} </option> ))} </select> </div> <div className="flex items-center space-x-6 lg:space-x-8"> <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 space-x-2"> <Button variant="outline" size="sm" onClick={() => table.setPageIndex(0)} disabled={!table.getCanPreviousPage()} > First </Button> <Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()} > Previous </Button> <Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()} > Next </Button> <Button variant="outline" size="sm" onClick={() => table.setPageIndex(table.getPageCount() - 1)} disabled={!table.getCanNextPage()} > Last </Button> </div> </div> </div> </div> )}
import * as React from "react"import { Slot } from "@radix-ui/react-slot"import { cva, type VariantProps } from "class-variance-authority"import { cn } from "@/lib/utils"const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, })function Button({ className, variant, size, asChild = false, ...props}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) { const Comp = asChild ? Slot : "button" return ( <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} /> )}export { Button, buttonVariants }
Multi-select with checkboxes
Bulk actions need row selection:
"use client"import * as React from "react"import { ColumnDef, flexRender, getCoreRowModel, useReactTable, RowSelectionState,} from "@tanstack/react-table"import { Button } from "@/components/ui/button"import { Checkbox } from "@/components/ui/checkbox"import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@/components/ui/table"import { Badge } from "@/components/ui/badge"const data: Payment[] = [ { id: "728ed52f", amount: 100, status: "pending", email: "[email protected]", }, { id: "489e1d42", amount: 125, status: "processing", email: "[email protected]", }, { id: "7c8f3a9b", amount: 75, status: "success", email: "[email protected]", }, { id: "a1b2c3d4", amount: 200, status: "failed", email: "[email protected]", }, { id: "e5f6g7h8", amount: 50, status: "success", email: "[email protected]", },]export type Payment = { id: string amount: number status: "pending" | "processing" | "success" | "failed" email: string}const getStatusColor = (status: string) => { switch (status) { case "success": return "default" case "processing": return "secondary" case "pending": return "outline" case "failed": return "destructive" default: return "outline" }}export const columns: ColumnDef<Payment>[] = [ { 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: "status", header: "Status", cell: ({ row }) => { const status = row.getValue("status") as string return ( <Badge variant={getStatusColor(status)}> {status} </Badge> ) }, }, { accessorKey: "email", header: "Email", cell: ({ row }) => <div className="lowercase">{row.getValue("email")}</div>, }, { 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">{formatted}</div> }, },]export default function DataTableRowSelection() { const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({}) const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), onRowSelectionChange: setRowSelection, state: { rowSelection, }, }) const selectedRows = table.getFilteredSelectedRowModel().rows const totalAmount = selectedRows.reduce((sum, row) => { return sum + (row.original.amount || 0) }, 0) return ( <div className="w-full p-6"> {selectedRows.length > 0 && ( <div className="flex items-center justify-between rounded-md border border-dashed p-4 mb-4"> <div className="flex items-center space-x-4"> <p className="text-sm font-medium"> {selectedRows.length} row(s) selected </p> <p className="text-sm text-muted-foreground"> Total: {new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }).format(totalAmount)} </p> </div> <div className="flex items-center space-x-2"> <Button variant="outline" size="sm" onClick={() => { // Handle bulk action console.log('Processing selected payments:', selectedRows.map(row => row.original)) }} > Process Selected </Button> <Button variant="outline" size="sm" onClick={() => setRowSelection({})} > Clear Selection </Button> </div> </div> )} <div className="overflow-hidden rounded-md border"> <Table> <TableHeader> {table.getHeaderGroups().map((headerGroup) => ( <TableRow key={headerGroup.id}> {headerGroup.headers.map((header) => { return ( <TableHead key={header.id}> {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> <div className="flex items-center justify-end space-x-2 py-4"> <div className="text-muted-foreground text-sm"> {selectedRows.length} of {data.length} row(s) selected. </div> </div> </div> )}
import * as React from "react"import { Slot } from "@radix-ui/react-slot"import { cva, type VariantProps } from "class-variance-authority"import { cn } from "@/lib/utils"const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, })function Button({ className, variant, size, asChild = false, ...props}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) { const Comp = asChild ? Slot : "button" return ( <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} /> )}export { Button, buttonVariants }
Show/hide columns
Let users customize which columns they see:
"use client"import * as React from "react"import { ColumnDef, VisibilityState, flexRender, getCoreRowModel, useReactTable,} from "@tanstack/react-table"import { ChevronDown, Eye, EyeOff } from "lucide-react"import { Button } from "@/components/ui/button"import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger,} from "@/components/ui/dropdown-menu"import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@/components/ui/table"import { Badge } from "@/components/ui/badge"const data: Payment[] = [ { id: "728ed52f", amount: 100, status: "pending", email: "[email protected]", customer: "John Doe", date: "2024-01-15", }, { id: "489e1d42", amount: 125, status: "processing", email: "[email protected]", customer: "Jane Smith", date: "2024-01-16", }, { id: "7c8f3a9b", amount: 75, status: "success", email: "[email protected]", customer: "Bob Johnson", date: "2024-01-17", }, { id: "a1b2c3d4", amount: 200, status: "failed", email: "[email protected]", customer: "Alice Brown", date: "2024-01-18", },]export type Payment = { id: string amount: number status: "pending" | "processing" | "success" | "failed" email: string customer: string date: string}const getStatusColor = (status: string) => { switch (status) { case "success": return "default" case "processing": return "secondary" case "pending": return "outline" case "failed": return "destructive" default: return "outline" }}export const columns: ColumnDef<Payment>[] = [ { accessorKey: "id", header: "ID", cell: ({ row }) => ( <div className="font-mono text-xs">{row.getValue("id")}</div> ), }, { accessorKey: "customer", header: "Customer", cell: ({ row }) => <div className="font-medium">{row.getValue("customer")}</div>, }, { accessorKey: "email", header: "Email", cell: ({ row }) => <div className="lowercase">{row.getValue("email")}</div>, }, { accessorKey: "status", header: "Status", cell: ({ row }) => { const status = row.getValue("status") as string return ( <Badge variant={getStatusColor(status)}> {status} </Badge> ) }, }, { accessorKey: "date", header: "Date", cell: ({ row }) => { const date = new Date(row.getValue("date")) return date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", }) }, }, { 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">{formatted}</div> }, },]export default function DataTableColumnVisibility() { const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({ id: false, // Hide ID column by default }) const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), onColumnVisibilityChange: setColumnVisibility, state: { columnVisibility, }, }) const visibleColumns = table.getAllColumns().filter(column => column.getIsVisible()) const hiddenColumns = table.getAllColumns().filter(column => !column.getIsVisible()) return ( <div className="w-full p-6"> <div className="flex items-center py-4"> <div className="flex items-center space-x-2"> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="outline" className="ml-auto"> <Eye className="mr-2 h-4 w-4" /> View <ChevronDown className="ml-2 h-4 w-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-[200px]"> <DropdownMenuLabel>Toggle columns</DropdownMenuLabel> <DropdownMenuSeparator /> {table .getAllColumns() .filter((column) => column.getCanHide()) .map((column) => { return ( <DropdownMenuCheckboxItem key={column.id} className="capitalize" checked={column.getIsVisible()} onCheckedChange={(value) => column.toggleVisibility(!!value) } > <div className="flex items-center space-x-2"> {column.getIsVisible() ? ( <Eye className="h-4 w-4" /> ) : ( <EyeOff className="h-4 w-4" /> )} <span>{column.id}</span> </div> </DropdownMenuCheckboxItem> ) })} </DropdownMenuContent> </DropdownMenu> </div> <div className="flex-1" /> <div className="flex items-center space-x-4 text-sm text-muted-foreground"> <span>{visibleColumns.length} visible</span> <span>{hiddenColumns.length} hidden</span> </div> </div> <div className="overflow-hidden rounded-md border"> <Table> <TableHeader> {table.getHeaderGroups().map((headerGroup) => ( <TableRow key={headerGroup.id}> {headerGroup.headers.map((header) => { return ( <TableHead key={header.id}> {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> </div> )}
import * as React from "react"import { Slot } from "@radix-ui/react-slot"import { cva, type VariantProps } from "class-variance-authority"import { cn } from "@/lib/utils"const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", destructive: "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, })function Button({ className, variant, size, asChild = false, ...props}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) { const Comp = asChild ? Slot : "button" return ( <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} /> )}export { Button, buttonVariants }
What components are available?
Table Components
The basic building blocks for any table:
Component | What it does |
---|---|
Table | Main container - handles responsive scrolling |
TableHeader | Where column headers live |
TableBody | Contains all your data rows |
TableFooter | Summary rows, totals, etc. |
TableRow | Individual row with hover and selection styling |
TableHead | Column header cells |
TableCell | Regular data cells |
TableCaption | Accessible description of the table |
Column Definition
Define what each column shows and how it behaves:
const columns: ColumnDef<YourDataType>[] = [
{
accessorKey: "propertyName", // Which data field to show
header: "Display Name", // Column title
cell: ({ row }) => <CustomCell />, // Custom formatting
enableSorting: true, // Clickable to sort?
enableHiding: true, // Can users hide it?
enableFiltering: true, // Include in search?
},
]
Table Configuration
The main hook that powers everything:
const table = useReactTable({
data, // Your array of data
columns, // Column definitions
getCoreRowModel: getCoreRowModel(), // Always required
// Add features as needed:
getSortedRowModel: getSortedRowModel(), // For sorting
getFilteredRowModel: getFilteredRowModel(), // For search/filters
getPaginationRowModel: getPaginationRowModel(), // For pagination
// Connect your state:
state: { sorting, columnFilters, rowSelection },
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
})
Data table best practices
What works best for real applications:
- Start simple, add features gradually - Basic table first, then sorting, then filters
- Always handle empty states - Show helpful messages when there's no data
- Make loading obvious - Users need to know when data is being fetched
- Keep column headers clear - "Email Address" is better than just "Email"
- Use consistent data types - Don't mix strings and numbers in the same column
- Implement keyboard navigation - Tab through controls, Enter to activate
- Test with real data volumes - Performance changes dramatically with 1000+ rows
- Consider mobile users - Tables get cramped, maybe show fewer columns on small screens
Context Menu
React context menu with right-click actions, submenus, and keyboard navigation. Perfect for file managers, editors, and interactive interfaces.
Date Picker
React date picker with calendar popup, keyboard navigation, and date formatting. Built with Popover and Calendar components for forms and scheduling.