DataTable
A composable, themeable and customizable data table component.
"use client"
/** * All Features Table Example * * This example demonstrates ALL available features of the DataTable: * - Multi-column sorting * - Advanced filtering (global search + column filters with AND/OR logic) * - Pagination * - Row selection with bulk actions * - Column visibility * - Row expansion * - Sidebar panels (left for filters, right for details) * - Data export (CSV) * - Controlled state management * - Selection bar with bulk actions */
import { useState, useCallback, useMemo } from "react"import type { PaginationState, SortingState, ColumnFiltersState, VisibilityState, RowSelectionState, ExpandedState, ColumnPinningState,} from "@tanstack/react-table"import { DataTableRoot, DataTable, DataTableHeader, DataTableBody, DataTableEmptyBody,} from "@/components/niko-table/core"import { DataTableToolbarSection, DataTablePagination, DataTableSearchFilter, DataTableViewMenu, DataTableSortMenu, DataTableFilterMenu, DataTableFacetedFilter, DataTableSliderFilter, DataTableClearFilter, DataTableEmptyIcon, DataTableEmptyMessage, DataTableEmptyFilteredMessage, DataTableEmptyTitle, DataTableEmptyDescription, DataTableEmptyActions, DataTableColumnHeader, DataTableColumnTitle, DataTableColumnActions, DataTableColumnSortOptions, DataTableColumnFacetedFilterOptions, DataTableColumnSliderFilterOptions, DataTableColumnDateFilterOptions, DataTableColumnPinOptions, DataTableColumnHideOptions, DataTableAside, DataTableAsideContent, DataTableAsideHeader, DataTableAsideTitle, DataTableAsideDescription, DataTableAsideClose, DataTableSelectionBar,} from "@/components/niko-table/components"import { SYSTEM_COLUMN_IDS, FILTER_VARIANTS } from "@/components/niko-table/lib"import { useDataTable } from "@/components/niko-table/core"import { daysAgo } from "@/components/niko-table/lib"import { exportTableToCSV } from "@/components/niko-table/filters"import type { DataTableColumnDef, ExtendedColumnFilter,} from "@/components/niko-table/types"import { Badge } from "@/components/ui/badge"import { Button } from "@/components/ui/button"import { Checkbox } from "@/components/ui/checkbox"import { SearchX, UserSearch } from "lucide-react"import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card"import { ScrollArea } from "@/components/ui/scroll-area"import { Separator } from "@/components/ui/separator"import { Download, Trash2, ChevronRight, ChevronDown, MoreHorizontal,} from "lucide-react"import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,} from "@/components/ui/dropdown-menu"
type Product = { id: string name: string category: string brand: string price: number stock: number rating: number inStock: boolean releaseDate: Date description: string tags: string[]}
const categoryOptions = [ { label: "Electronics", value: "electronics" }, { label: "Clothing", value: "clothing" }, { label: "Home & Garden", value: "home-garden" }, { label: "Sports", value: "sports" }, { label: "Books", value: "books" },]
const brandOptions = [ { label: "Apple", value: "apple" }, { label: "Samsung", value: "samsung" }, { label: "Nike", value: "nike" }, { label: "Adidas", value: "adidas" }, { label: "Sony", value: "sony" }, { label: "LG", value: "lg" }, { label: "Dell", value: "dell" }, { label: "HP", value: "hp" },]
const initialData: Product[] = [ { id: "1", name: "iPhone 15 Pro", category: "electronics", brand: "apple", price: 999, stock: 45, rating: 5, inStock: true, releaseDate: daysAgo(5), description: "Latest iPhone with A17 Pro chip and titanium design", tags: ["premium", "new", "smartphone"], }, { id: "2", name: "Galaxy S24 Ultra", category: "electronics", brand: "samsung", price: 1199, stock: 32, rating: 5, inStock: true, releaseDate: daysAgo(10), description: "Flagship Android phone with S Pen and AI features", tags: ["premium", "new", "smartphone"], }, { id: "3", name: "Air Jordan 1", category: "sports", brand: "nike", price: 170, stock: 8, rating: 4, inStock: true, releaseDate: daysAgo(25), description: "Classic basketball sneakers with iconic design", tags: ["sneakers", "basketball", "classic"], }, { id: "4", name: "Ultraboost 23", category: "sports", brand: "adidas", price: 190, stock: 15, rating: 4, inStock: true, releaseDate: daysAgo(50), description: "Running shoes with Boost technology", tags: ["running", "comfort", "athletic"], }, { id: "5", name: "PlayStation 5", category: "electronics", brand: "sony", price: 499, stock: 0, rating: 5, inStock: false, releaseDate: daysAgo(365), description: "Next-gen gaming console with ray tracing", tags: ["gaming", "console", "entertainment"], }, { id: "6", name: "OLED C3 TV", category: "electronics", brand: "lg", price: 1499, stock: 12, rating: 5, inStock: true, releaseDate: daysAgo(90), description: "55-inch OLED TV with perfect blacks", tags: ["tv", "entertainment", "premium"], }, { id: "7", name: "XPS 15 Laptop", category: "electronics", brand: "dell", price: 1899, stock: 20, rating: 4, inStock: true, releaseDate: daysAgo(120), description: "Premium laptop for professionals", tags: ["laptop", "professional", "premium"], }, { id: "8", name: "Spectre x360", category: "electronics", brand: "hp", price: 1599, stock: 18, rating: 4, inStock: true, releaseDate: daysAgo(15), description: "2-in-1 convertible laptop", tags: ["laptop", "convertible", "versatile"], }, { id: "9", name: "MacBook Pro 16", category: "electronics", brand: "apple", price: 2499, stock: 25, rating: 5, inStock: true, releaseDate: daysAgo(30), description: "Powerful laptop for creative professionals", tags: ["laptop", "professional", "creative"], }, { id: "10", name: "Galaxy Book3", category: "electronics", brand: "samsung", price: 1399, stock: 14, rating: 4, inStock: true, releaseDate: daysAgo(180), description: "Sleek Windows laptop", tags: ["laptop", "windows", "sleek"], }, { id: "11", name: "Running Shorts", category: "clothing", brand: "nike", price: 45, stock: 120, rating: 3, inStock: true, releaseDate: daysAgo(60), description: "Comfortable running shorts", tags: ["clothing", "running", "athletic"], }, { id: "12", name: "Training Jacket", category: "clothing", brand: "adidas", price: 85, stock: 65, rating: 4, inStock: true, releaseDate: daysAgo(45), description: "Lightweight training jacket", tags: ["clothing", "training", "athletic"], }, { id: "13", name: "Garden Tools Set", category: "home-garden", brand: "hp", price: 120, stock: 30, rating: 4, inStock: true, releaseDate: daysAgo(75), description: "Complete set of gardening tools", tags: ["tools", "garden", "home"], }, { id: "14", name: "Programming Book", category: "books", brand: "dell", price: 60, stock: 50, rating: 5, inStock: true, releaseDate: daysAgo(200), description: "Learn React and TypeScript", tags: ["book", "programming", "education"], }, { id: "15", name: "Wireless Mouse", category: "electronics", brand: "lg", price: 35, stock: 200, rating: 3, inStock: true, releaseDate: daysAgo(150), description: "Ergonomic wireless mouse", tags: ["accessories", "computer", "wireless"], },]
// Expanded row content componentfunction ExpandedRowContent({ product }: { product: Product }) { return ( <div className="bg-muted/30 p-4"> <div className="space-y-3"> <div> <h4 className="mb-2 text-sm font-semibold">Description</h4> <p className="text-sm text-muted-foreground">{product.description}</p> </div> <div> <h4 className="mb-2 text-sm font-semibold">Tags</h4> <div className="flex flex-wrap gap-2"> {product.tags.map(tag => ( <Badge key={tag} variant="secondary" className="text-xs"> {tag} </Badge> ))} </div> </div> </div> </div> )}
// Product details component for sidebarfunction ProductDetails({ product }: { product: Product }) { return ( <ScrollArea className="h-full"> <div className="space-y-6 p-6"> <div> <h2 className="text-2xl font-bold">{product.name}</h2> <p className="mt-1 text-sm text-muted-foreground"> {categoryOptions.find(opt => opt.value === product.category)?.label} </p> </div>
<Separator />
<div className="space-y-4"> <div> <h3 className="mb-2 text-sm font-semibold">Details</h3> <div className="space-y-2 text-sm"> <div className="flex justify-between"> <span className="text-muted-foreground">Brand:</span> <span> {brandOptions.find(opt => opt.value === product.brand)?.label} </span> </div> <div className="flex justify-between"> <span className="text-muted-foreground">Price:</span> <span className="font-medium">${product.price.toFixed(2)}</span> </div> <div className="flex justify-between"> <span className="text-muted-foreground">Stock:</span> <span className={ product.stock < 10 ? "font-medium text-red-600" : "" } > {product.stock} units </span> </div> <div className="flex justify-between"> <span className="text-muted-foreground">Rating:</span> <div className="flex items-center gap-1"> <span>{product.rating}</span> <span className="text-yellow-500">★</span> </div> </div> <div className="flex justify-between"> <span className="text-muted-foreground">Status:</span> <Badge variant={product.inStock ? "default" : "secondary"}> {product.inStock ? "In Stock" : "Out of Stock"} </Badge> </div> <div className="flex justify-between"> <span className="text-muted-foreground">Release Date:</span> <span>{product.releaseDate.toLocaleDateString()}</span> </div> </div> </div>
<Separator />
<div> <h3 className="mb-2 text-sm font-semibold">Description</h3> <p className="text-sm text-muted-foreground"> {product.description} </p> </div>
<Separator />
<div> <h3 className="mb-2 text-sm font-semibold">Tags</h3> <div className="flex flex-wrap gap-2"> {product.tags.map(tag => ( <Badge key={tag} variant="outline" className="text-xs"> {tag} </Badge> ))} </div> </div> </div> </div> </ScrollArea> )}
// Bulk actions componentfunction BulkActions() { const { table } = useDataTable<Product>() const selectedRows = table.getFilteredSelectedRowModel().rows const selectedCount = selectedRows.length
const handleBulkExport = () => { exportTableToCSV(table, { filename: "selected-products", excludeColumns: [ "select", "expand", "actions", ] as unknown as (keyof Product)[], onlySelected: true, }) }
const handleBulkDelete = () => { // In a real app, you would delete the selected items console.log( "Deleting:", selectedRows.map(row => row.original.id), ) table.resetRowSelection() }
return ( <DataTableSelectionBar selectedCount={selectedCount} onClear={() => table.resetRowSelection()} > <Button size="sm" variant="outline" onClick={handleBulkExport}> <Download className="mr-2 h-4 w-4" /> Export Selected </Button> <Button size="sm" variant="destructive" onClick={handleBulkDelete}> <Trash2 className="mr-2 h-4 w-4" /> Delete Selected </Button> </DataTableSelectionBar> )}
// Filter toolbar componentfunction FilterToolbar({ filters, onFiltersChange,}: { filters: ExtendedColumnFilter<Product>[] onFiltersChange: (filters: ExtendedColumnFilter<Product>[] | null) => void}) { return ( <DataTableToolbarSection className="w-full flex-col justify-between gap-2"> <DataTableToolbarSection className="px-0"> <DataTableSearchFilter placeholder="Search products..." /> <DataTableViewMenu /> </DataTableToolbarSection> <DataTableToolbarSection className="flex-wrap px-0"> <DataTableFacetedFilter accessorKey="category" title="Category" options={categoryOptions} multiple /> <DataTableFacetedFilter accessorKey="brand" title="Brand" options={brandOptions} limitToFilteredRows multiple /> <DataTableSliderFilter accessorKey="price" /> <DataTableSortMenu /> <DataTableFilterMenu filters={filters} onFiltersChange={onFiltersChange} /> <DataTableClearFilter /> </DataTableToolbarSection> </DataTableToolbarSection> )}
export default function AllFeaturesTableExample() { // Controlled state management const [data] = useState<Product[]>(initialData) const [globalFilter, setGlobalFilter] = useState<string | object>("") const [sorting, setSorting] = useState<SortingState>([]) const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]) const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({}) const [rowSelection, setRowSelection] = useState<RowSelectionState>({}) const [expanded, setExpanded] = useState<ExpandedState>({}) const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 10, }) const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({ left: [], right: [], })
// Sidebar state const [selectedProductId, setSelectedProductId] = useState<string | null>( null, )
const selectedProduct = selectedProductId ? data.find(product => product.id === selectedProductId) : null
const resetAllState = useCallback(() => { setGlobalFilter("") setSorting([]) setColumnFilters([]) setColumnVisibility({}) setRowSelection({}) setExpanded({}) setColumnPinning({ left: [], right: [] }) setPagination({ pageIndex: 0, pageSize: 10 }) setSelectedProductId(null) }, [])
// Extract filters for display const currentFilters = useMemo(() => { if ( typeof globalFilter === "object" && globalFilter && "filters" in globalFilter ) { const filterObj = globalFilter as { filters: ExtendedColumnFilter<Product>[] } return filterObj.filters || [] } return columnFilters .map(cf => cf.value) .filter( (v): v is ExtendedColumnFilter<Product> => v !== null && typeof v === "object" && "id" in v, ) }, [globalFilter, columnFilters])
// Handler for filter menu const handleFiltersChange = useCallback( (filters: ExtendedColumnFilter<Product>[] | null) => { if (!filters || filters.length === 0) { setColumnFilters([]) setGlobalFilter("") setPagination(prev => ({ ...prev, pageIndex: 0 })) } else { const hasOrFilters = filters.some( (filter, index) => index > 0 && filter.joinOperator === "or", ) if (hasOrFilters) { setColumnFilters([]) setGlobalFilter({ filters, joinOperator: "mixed", }) setPagination(prev => ({ ...prev, pageIndex: 0 })) } else { setGlobalFilter("") setColumnFilters( filters.map(filter => ({ id: filter.id, value: filter, })), ) setPagination(prev => ({ ...prev, pageIndex: 0 })) } } }, [], )
// Define columns with all features const columns: DataTableColumnDef<Product>[] = useMemo( () => [ { id: SYSTEM_COLUMN_IDS.SELECT, size: 40, // Compact width for checkbox column 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, }, { id: SYSTEM_COLUMN_IDS.EXPAND, header: () => null, cell: ({ row }) => { if (!row.getCanExpand()) return null return ( <Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={row.getToggleExpandedHandler()} > {row.getIsExpanded() ? ( <ChevronDown className="h-4 w-4" /> ) : ( <ChevronRight className="h-4 w-4" /> )} </Button> ) }, size: 50, enableSorting: false, enableHiding: false, meta: { expandedContent: (product: Product) => ( <ExpandedRowContent product={product} /> ), }, }, { accessorKey: "name", header: () => ( <DataTableColumnHeader className="justify-start"> <DataTableColumnTitle>Product Name</DataTableColumnTitle> <DataTableColumnActions> <DataTableColumnSortOptions withSeparator={false} /> <DataTableColumnPinOptions /> <DataTableColumnHideOptions /> </DataTableColumnActions> </DataTableColumnHeader> ), meta: { label: "Product Name", variant: FILTER_VARIANTS.TEXT, }, enableColumnFilter: true, cell: ({ row }) => ( <div className="cursor-pointer font-medium hover:underline" onClick={() => { setSelectedProductId(row.original.id) }} > {row.getValue("name")} </div> ), }, { accessorKey: "category", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> {/* Composable Actions: Multi-select filter example */} <DataTableColumnActions label="Category Options"> <DataTableColumnSortOptions variant={FILTER_VARIANTS.TEXT} withSeparator={false} /> <DataTableColumnFacetedFilterOptions options={categoryOptions} multiple /> <DataTableColumnPinOptions /> <DataTableColumnHideOptions /> </DataTableColumnActions> </DataTableColumnHeader> ), meta: { label: "Category", variant: FILTER_VARIANTS.SELECT, options: categoryOptions, }, cell: ({ row }) => { const category = row.getValue("category") as string const option = categoryOptions.find(opt => opt.value === category) return <span>{option?.label || category}</span> }, enableColumnFilter: true, }, { accessorKey: "brand", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> {/* Composable Actions: Single-select filter example */} <DataTableColumnActions label="Brand Options"> <DataTableColumnSortOptions variant={FILTER_VARIANTS.TEXT} withSeparator={false} /> <DataTableColumnFacetedFilterOptions options={brandOptions} multiple={false} /> <DataTableColumnPinOptions /> <DataTableColumnHideOptions /> </DataTableColumnActions> </DataTableColumnHeader> ), meta: { label: "Brand", variant: FILTER_VARIANTS.SELECT, options: brandOptions, }, enableColumnFilter: true, }, { accessorKey: "price", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnActions> <DataTableColumnSortOptions withSeparator={false} /> <DataTableColumnSliderFilterOptions /> <DataTableColumnPinOptions /> <DataTableColumnHideOptions /> </DataTableColumnActions> </DataTableColumnHeader> ), meta: { label: "Price", unit: "$", variant: FILTER_VARIANTS.RANGE, }, cell: ({ row }) => { const price = parseFloat(row.getValue("price")) return <div className="font-medium">${price.toFixed(2)}</div> }, enableColumnFilter: true, }, { accessorKey: "stock", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> {/* All actions composed in single dropdown */} <DataTableColumnActions> <DataTableColumnSortOptions variant={FILTER_VARIANTS.NUMBER} withSeparator={false} /> <DataTableColumnPinOptions /> <DataTableColumnHideOptions /> </DataTableColumnActions> </DataTableColumnHeader> ), meta: { label: "Stock", variant: FILTER_VARIANTS.NUMBER, }, cell: ({ row }) => { const stock = Number(row.getValue("stock")) return ( <div className={stock < 10 ? "font-medium text-red-600" : ""}> {stock} </div> ) }, enableColumnFilter: true, }, { accessorKey: "rating", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnActions> <DataTableColumnSortOptions variant={FILTER_VARIANTS.NUMBER} withSeparator={false} /> <DataTableColumnPinOptions /> <DataTableColumnHideOptions /> </DataTableColumnActions> </DataTableColumnHeader> ), meta: { label: "Rating", variant: FILTER_VARIANTS.NUMBER, }, cell: ({ row }) => { const rating = Number(row.getValue("rating")) return ( <div className="flex items-center gap-1"> <span>{rating}</span> <span className="text-yellow-500">★</span> </div> ) }, enableColumnFilter: true, }, { accessorKey: "inStock", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnActions> <DataTableColumnSortOptions withSeparator={false} /> <DataTableColumnPinOptions /> <DataTableColumnHideOptions /> </DataTableColumnActions> </DataTableColumnHeader> ), meta: { label: "In Stock", variant: FILTER_VARIANTS.BOOLEAN, }, cell: ({ row }) => { const inStock = Boolean(row.getValue("inStock")) return ( <Badge variant={inStock ? "default" : "secondary"}> {inStock ? "Yes" : "No"} </Badge> ) }, enableColumnFilter: true, }, { accessorKey: "releaseDate", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnActions> <DataTableColumnSortOptions withSeparator={false} /> <DataTableColumnDateFilterOptions /> <DataTableColumnPinOptions /> <DataTableColumnHideOptions /> </DataTableColumnActions> </DataTableColumnHeader> ), meta: { label: "Release Date", variant: FILTER_VARIANTS.DATE, }, cell: ({ row }) => { const date = row.getValue("releaseDate") as Date return <span>{date.toLocaleDateString()}</span> }, enableColumnFilter: true, }, { id: "actions", header: () => <div className="text-right">Actions</div>, cell: ({ row }) => { const product = row.original return ( <div className="flex justify-end"> <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={() => { setSelectedProductId(product.id) }} > View Details </DropdownMenuItem> <DropdownMenuItem onClick={() => console.log("Edit", product.id)} > Edit </DropdownMenuItem> <DropdownMenuItem onClick={() => console.log("Delete", product.id)} className="text-red-600" > Delete </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> </div> ) }, enableSorting: false, enableHiding: false, }, ], [], )
return ( <div className="w-full space-y-4"> <DataTableRoot data={data} columns={columns} config={{ enablePagination: true, enableSorting: true, enableMultiSort: true, enableFilters: true, enableRowSelection: true, enableExpanding: true, }} getRowCanExpand={() => true} getSubRows={() => undefined} state={{ globalFilter, sorting, columnFilters, columnVisibility, rowSelection, expanded, columnPinning, pagination, }} onGlobalFilterChange={value => { setGlobalFilter(value) setPagination(prev => ({ ...prev, pageIndex: 0 })) }} onSortingChange={setSorting} onColumnFiltersChange={setColumnFilters} onColumnVisibilityChange={setColumnVisibility} onRowSelectionChange={setRowSelection} onExpandedChange={setExpanded} onColumnPinningChange={setColumnPinning} onPaginationChange={setPagination} > <FilterToolbar filters={currentFilters} onFiltersChange={handleFiltersChange} /> <BulkActions />
{/* Sidebar Layout */} <div className="flex min-h-150 gap-4"> {/* Main Table Area */} <DataTable className="flex-1" height="100%"> <DataTableHeader /> <DataTableBody onRowClick={(product: Product) => { console.log("Row clicked:", product.id) setSelectedProductId(product.id) }} > <DataTableEmptyBody> <DataTableEmptyMessage> <DataTableEmptyIcon> <UserSearch className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No products found</DataTableEmptyTitle> <DataTableEmptyDescription> Get started by adding your first product to the inventory. </DataTableEmptyDescription> </DataTableEmptyMessage> <DataTableEmptyFilteredMessage> <DataTableEmptyIcon> <SearchX className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No matches found</DataTableEmptyTitle> <DataTableEmptyDescription> Try adjusting your filters or search to find what you're looking for. </DataTableEmptyDescription> </DataTableEmptyFilteredMessage> <DataTableEmptyActions> <Button onClick={() => alert("Add product clicked")}> Add Product </Button> </DataTableEmptyActions> </DataTableEmptyBody> </DataTableBody> </DataTable>
{/* Right Sidebar - Product Details */} {selectedProduct && ( <DataTableAside side="right" open={!!selectedProduct} onOpenChange={open => { if (!open) setSelectedProductId(null) }} > <DataTableAsideContent width="w-78"> <DataTableAsideHeader> <DataTableAsideTitle>Product Details</DataTableAsideTitle> <DataTableAsideDescription> View detailed information </DataTableAsideDescription> <DataTableAsideClose /> </DataTableAsideHeader> <ProductDetails product={selectedProduct} /> </DataTableAsideContent> </DataTableAside> )} </div> <DataTablePagination /> </DataTableRoot>
{/* State Display */} <Card> <CardHeader> <CardTitle>Current Table State</CardTitle> <CardDescription> Live view of all table state for demonstration </CardDescription> <CardAction> <Button variant="outline" size="sm" onClick={resetAllState}> Reset All State </Button> </CardAction> </CardHeader> <CardContent className="space-y-4"> <div className="grid gap-2 text-xs text-muted-foreground"> <div className="flex justify-between"> <span className="font-medium">Total Items:</span> <span className="text-foreground">{data.length}</span> </div> <div className="flex justify-between"> <span className="font-medium">Selected Rows:</span> <span className="text-foreground"> { Object.keys(rowSelection).filter(key => rowSelection[key]) .length } </span> </div> <div className="flex justify-between"> <span className="font-medium">Expanded Rows:</span> <span className="text-foreground"> {typeof expanded === "object" && expanded !== null ? Object.keys(expanded).filter( key => (expanded as Record<string, boolean>)[key], ).length : 0} </span> </div> <div className="flex justify-between"> <span className="font-medium">Active Filters:</span> <span className="text-foreground">{currentFilters.length}</span> </div> <div className="flex justify-between"> <span className="font-medium">Sorting:</span> <span className="text-foreground"> {sorting.length > 0 ? sorting .map(s => `${s.id} ${s.desc ? "desc" : "asc"}`) .join(", ") : "None"} </span> </div> <div className="flex justify-between"> <span className="font-medium">Page:</span> <span className="text-foreground"> {pagination.pageIndex + 1} (Size: {pagination.pageSize}) </span> </div> <div className="flex justify-between"> <span className="font-medium">Pinned Columns:</span> <span className="text-foreground"> {columnPinning.left?.length || 0} Left,{" "} {columnPinning.right?.length || 0} Right </span> </div> </div> </CardContent> </Card> </div> )}A data table with sorting, filtering, pagination, row selection, and more.
Data tables are one of the most complex components to build. They are central to any application and often contain a lot of moving parts.
I don’t like building data tables. So I built 30+ of them. All kinds of configurations. Then I extracted the core components into data-table.
We now have a solid foundation to build on top of. Composable. Themeable. Customizable.
Installation
Section titled “Installation”-
Run the following command to install the required Shadcn components
npx shadcn@latest add table input button dropdown-menu popover command badge scroll-area separator checkbox select skeleton tooltip -
Add the TanStack Table dependency
npm install @tanstack/react-table -
Copy the DataTable components into your project
See the Installation Guide for detailed instructions on copying the DataTable components.
Structure
Section titled “Structure”A DataTable component is composed of the following parts:
DataTableRoot- The root provider that manages table state and context.DataTableToolbarSection- Container for filters, search, and actions.DataTable- The table container component.DataTableHeader- The table header with sortable columns.DataTableBody- The table body with rows.DataTablePagination- Pagination controls.
┌─────────────────────────────────────────────────────────────────┐│ DataTableRoot ││ ┌───────────────────────────────────────────────────────────┐ ││ │ DataTableToolbarSection │ ││ │ ┌─────────────────┐ ┌─────────────┐ ┌─────────────────┐ │ ││ │ │ SearchFilter │ │ FilterMenu │ │ SortMenu/View │ │ ││ │ └─────────────────┘ └─────────────┘ └─────────────────┘ │ ││ └───────────────────────────────────────────────────────────┘ ││ ┌───────────────────────────────────────────────────────────┐ ││ │ DataTable │ ││ │ ┌─────────────────────────────────────────────────────┐ │ ││ │ │ DataTableHeader (sticky) │ │ ││ │ │ ┌─────────┬─────────┬─────────┬─────────────────┐ │ │ ││ │ │ │ Column │ Column │ Column │ Column │ │ │ ││ │ │ └─────────┴─────────┴─────────┴─────────────────┘ │ │ ││ │ └─────────────────────────────────────────────────────┘ │ ││ │ ┌─────────────────────────────────────────────────────┐ │ ││ │ │ DataTableBody (scrollable) │ │ ││ │ │ ┌─────────┬─────────┬─────────┬─────────────────┐ │ │ ││ │ │ │ Cell │ Cell │ Cell │ Cell │ │ │ ││ │ │ ├─────────┼─────────┼─────────┼─────────────────┤ │ │ ││ │ │ │ Cell │ Cell │ Cell │ Cell │ │ │ ││ │ │ └─────────┴─────────┴─────────┴─────────────────┘ │ │ ││ │ │ │ │ ││ │ │ DataTableSkeleton (when loading) │ │ ││ │ │ DataTableEmptyBody (when no data) │ │ ││ │ └─────────────────────────────────────────────────────┘ │ ││ └───────────────────────────────────────────────────────────┘ ││ ┌───────────────────────────────────────────────────────────┐ ││ │ DataTablePagination │ ││ │ ┌─────────────┐ ┌───────────────────┐ ┌─────────────┐ │ ││ │ │ Page Size │ │ Page 1 of 10 │ │ Navigation │ │ ││ │ └─────────────┘ └───────────────────┘ └─────────────┘ │ ││ └───────────────────────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────────────┘import { DataTableRoot, DataTable, DataTableHeader, DataTableBody, DataTableEmptyBody, DataTableSkeleton,} from "@/components/niko-table/core"import { DataTableToolbarSection, DataTableSearchFilter, DataTablePagination,} from "@/components/niko-table/components"import type { DataTableColumnDef } from "@/components/niko-table/types"
type User = { id: string name: string email: string}
const columns: DataTableColumnDef<User>[] = [ { accessorKey: "name", header: "Name" }, { accessorKey: "email", header: "Email" },]
export function UsersTable({ data }: { data: User[] }) { return ( <DataTableRoot data={data} columns={columns}> <DataTableToolbarSection> <DataTableSearchFilter placeholder="Search users..." /> </DataTableToolbarSection> <DataTable> <DataTableHeader /> <DataTableBody> <DataTableSkeleton /> <DataTableEmptyBody /> </DataTableBody> </DataTable> <DataTablePagination /> </DataTableRoot> )}Your First Table
Section titled “Your First Table”Let’s start with the most basic table. A simple table with data.
-
Create your column definitions
columns.tsx import type { DataTableColumnDef } from "@/components/niko-table/types"type User = {id: stringname: stringemail: string}export const columns: DataTableColumnDef<User>[] = [{accessorKey: "name",header: "Name",},{accessorKey: "email",header: "Email",},] -
Create your table component
users-table.tsx import {DataTableRoot,DataTable,DataTableHeader,DataTableBody,} from "@/components/niko-table/core"import { columns } from "./columns"export function UsersTable({ data }: { data: User[] }) {return (<DataTableRoot data={data} columns={columns}><DataTable><DataTableHeader /><DataTableBody /></DataTable></DataTableRoot>)} -
Add loading and empty states
users-table.tsx import {DataTableRoot,DataTable,DataTableHeader,DataTableBody,DataTableSkeleton,DataTableEmptyBody,} from "@/components/niko-table/core"export function UsersTable({data,isLoading,}: {data: User[]isLoading?: boolean}) {return (<DataTableRoot data={data} columns={columns} isLoading={isLoading}><DataTable><DataTableHeader /><DataTableBody><DataTableSkeleton /><DataTableEmptyBody /></DataTableBody></DataTable></DataTableRoot>)} -
Add search and pagination
users-table.tsx import {DataTableRoot,DataTable,DataTableHeader,DataTableBody,DataTableSkeleton,DataTableEmptyBody,} from "@/components/niko-table/core"import {DataTableToolbarSection,DataTableSearchFilter,DataTablePagination,} from "@/components/niko-table/components"export function UsersTable({data,isLoading,}: {data: User[]isLoading?: boolean}) {return (<DataTableRoot data={data} columns={columns} isLoading={isLoading}><DataTableToolbarSection><DataTableSearchFilter placeholder="Search users..." /></DataTableToolbarSection><DataTable><DataTableHeader /><DataTableBody><DataTableSkeleton /><DataTableEmptyBody /></DataTableBody></DataTable><DataTablePagination /></DataTableRoot>)} -
You’ve created your first table!
Your table now has search functionality and pagination. See the Examples for more advanced configurations.
Components
Section titled “Components”The components in data-table are built to be composable i.e you build your table by putting the provided components together. They also compose well with other shadcn/ui components such as DropdownMenu, Popover or Dialog etc.
If you need to change the code in data-table, you are encouraged to do so. The code is yours. Use data-table as a starting point and build your own.
See the Components page for detailed documentation on each component.
DataTableRoot
Section titled “DataTableRoot”The DataTableRoot component is used to provide the table context to all child components. You should always wrap your table in a DataTableRoot component.
| Name | Type | Description |
|---|---|---|
data | TData[] | The data array to display in the table. |
columns | DataTableColumnDef<TData>[] | Column definitions array. |
table | Table<TData> | Pre-configured TanStack Table instance (optional). |
config | DataTableConfig | Configuration object for feature toggles. |
state | Partial<TableState> | Controlled table state (pagination, sorting, etc). |
isLoading | boolean | Loading state for the table. |
getRowId | (row: TData, index: number) => string | Custom function to get row IDs. |
onGlobalFilterChange | (value: GlobalFilter) => void | Callback when global filter changes. |
onPaginationChange | (updater: Updater<PaginationState>) => void | Callback when pagination changes. |
onSortingChange | (updater: Updater<SortingState>) => void | Callback when sorting changes. |
onColumnFiltersChange | (updater: Updater<ColumnFiltersState>) => void | Callback when column filters change. |
onColumnVisibilityChange | (updater: Updater<VisibilityState>) => void | Callback when column visibility changes. |
onRowSelectionChange | (updater: Updater<RowSelectionState>) => void | Callback when row selection changes. |
onExpandedChange | (updater: Updater<ExpandedState>) => void | Callback when expanded state changes. |
onRowSelection | (selectedRows: TData[]) => void | Callback with selected row data. |
Config
Section titled “Config”The config prop accepts a DataTableConfig object:
| Name | Type | Default | Description |
|---|---|---|---|
enablePagination | boolean | true | Enable pagination. |
enableFilters | boolean | true | Enable filtering. |
enableSorting | boolean | true | Enable sorting. |
enableRowSelection | boolean | false | Enable row selection. |
enableMultiSort | boolean | true | Enable multi-column sorting. |
enableGrouping | boolean | false | Enable column grouping. |
enableExpanding | boolean | false | Enable row expansion. |
manualSorting | boolean | false | Enable server-side sorting. |
manualPagination | boolean | false | Enable server-side pagination. |
manualFiltering | boolean | false | Enable server-side filtering. |
pageCount | number | - | Total pages (for server-side pagination). |
initialPageSize | number | 10 | Initial page size. |
initialPageIndex | number | 0 | Initial page index. |
autoResetPageIndex | boolean | - | Auto-reset page on filter/sort. Defaults to false when manualPagination: true. |
autoResetExpanded | boolean | true | Auto-reset expanded rows on filter/sort change. |
useDataTable
Section titled “useDataTable”The useDataTable hook is used to access the table instance from any child component.
import { useDataTable } from "@/components/niko-table/core"
export function CustomComponent() { const { table, isLoading } = useDataTable()
return ( <div> <p>Total rows: {table.getFilteredRowModel().rows.length}</p> <p>Loading: {isLoading ? "Yes" : "No"}</p> </div> )}Return Values
Section titled “Return Values”| Property | Type | Description |
|---|---|---|
table | DataTableInstance<TData> | The TanStack Table instance. |
columns | DataTableColumnDef<TData>[] | The column definitions. |
isLoading | boolean | Whether the table is in a loading state. |
setIsLoading | (isLoading: boolean) => void | Programmatically set the loading state. |
Controlled State
Section titled “Controlled State”Use the state callbacks to control the table externally:
import { useState } from "react"import type { SortingState, PaginationState } from "@tanstack/react-table"
export function ControlledTable({ data }: { data: User[] }) { const [sorting, setSorting] = useState<SortingState>([]) const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 10, })
return ( <DataTableRoot data={data} columns={columns} state={{ sorting, pagination }} onSortingChange={setSorting} onPaginationChange={setPagination} > {/* ... */} </DataTableRoot> )}Server-Side Data
Section titled “Server-Side Data”For server-side pagination, sorting, and filtering:
export function ServerSideTable() { const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 })
const { data, totalCount, isLoading } = useQuery({ queryKey: ["users", pagination], queryFn: () => fetchUsers(pagination), })
const pageCount = Math.ceil(totalCount / pagination.pageSize)
return ( <DataTableRoot data={data ?? []} columns={columns} config={{ manualPagination: true, pageCount, }} isLoading={isLoading} onPaginationChange={setPagination} > <DataTable> <DataTableHeader /> <DataTableBody> <DataTableSkeleton /> <DataTableEmptyBody /> </DataTableBody> </DataTable> <DataTablePagination totalCount={totalCount} /> </DataTableRoot> )}Data Fetching
Section titled “Data Fetching”React Query
Section titled “React Query”function UsersTable() { const { data, isLoading } = useQuery({ queryKey: ["users"], queryFn: fetchUsers, })
return ( <DataTableRoot data={data ?? []} columns={columns} isLoading={isLoading}> <DataTable> <DataTableHeader /> <DataTableBody> <DataTableSkeleton /> <DataTableEmptyBody /> </DataTableBody> </DataTable> </DataTableRoot> )}function UsersTable() { const { data, isLoading } = useSWR("/api/users", fetcher)
return ( <DataTableRoot data={data ?? []} columns={columns} isLoading={isLoading}> <DataTable> <DataTableHeader /> <DataTableBody> <DataTableSkeleton /> <DataTableEmptyBody /> </DataTableBody> </DataTable> </DataTableRoot> )}Column Meta
Section titled “Column Meta”Columns support metadata for advanced filtering, sorting, and display:
const columns: DataTableColumnDef<Product>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Product Name", placeholder: "Search products...", variant: "text", }, enableColumnFilter: true, enableSorting: true, }, { accessorKey: "category", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Category", variant: "select", options: [ { label: "Electronics", value: "electronics" }, { label: "Clothing", value: "clothing" }, ], }, enableColumnFilter: true, }, { accessorKey: "price", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Price", variant: "range", unit: "$", }, enableColumnFilter: true, },]Filter Variants
Section titled “Filter Variants”| Variant | Description |
|---|---|
text | Text input filter. |
number | Number input filter. |
select | Single selection dropdown. |
multi_select | Multiple selection dropdown. |
range | Numeric range slider. |
date | Single date picker. |
date_range | Date range picker. |
boolean | Boolean toggle. |
Next Steps
Section titled “Next Steps”- Components - Detailed component documentation
- Simple Table - Basic table example
- Basic Table - Table with pagination and sorting
- Advanced Table - Complex filtering and sorting
- Server-Side Table - Server-side data handling
The components in data-table are built to be composable. You build your table by putting the provided components together. They also compose well with other shadcn/ui components.
If you need to change the code, you are encouraged to do so. The code is yours.
Directory Structure
Section titled “Directory Structure”The Niko Table components are organized into logical directories following the file structure in src/components/niko-table:
core/- Essential table components (DataTableRoot, DataTable, context, structure components)components/- User-facing context-aware components (automatically connect to table context viauseDataTablehook)filters/- Core filter implementation components (accepttableprop directly, used by components/)hooks/- Custom React hooks for table functionalitylib/- Utility functions and constantstypes/- TypeScript type definitionsconfig/- Configuration and feature detection
This documentation follows this structure for easy navigation. Each component section includes links to source code and relevant documentation.
Documentation Sections
Section titled “Documentation Sections”Essential building blocks of the data table. They handle table initialization, context management, and basic structure.
Components:
DataTableRoot- Provides table context and initializes TanStack TableDataTable- Main table container with scrolling behaviorDataTableHeader- Table header with sortable columnsDataTableBody- Table body with rows and scroll eventsDataTableSkeleton- Loading skeletonDataTableEmptyBody- Empty state componentDataTableLoading- Loading indicatorDataTableErrorBoundary- Error boundary for table- Virtualized components for large datasets
Context-aware components that automatically connect to the table via the useDataTable hook. These are the recommended components for most use cases.
Components:
DataTableToolbarSection- Container for filters and actionsDataTablePagination- Full-featured pagination controlsDataTableSearchFilter- Global search input with debouncingDataTableFilterMenu- Command palette-style filter interfaceDataTableFacetedFilter- Faceted filter for single/multiple selectionDataTableSortMenu- Sort management with drag-and-dropDataTableViewMenu- Column visibility toggleDataTableInlineFilter- Inline filter toolbarDataTableSliderFilter- Slider filter for numeric rangesDataTableDateFilter- Date filter componentDataTableClearFilter- Clear all filters buttonDataTableExportButton- Export to CSV buttonDataTableColumnHeader- Sortable column headerDataTableColumnFacetedFilterMenu- Column-level faceted filter popoverDataTableColumnSliderFilterMenu- Column-level slider filter popoverDataTableColumnDateFilterMenu- Column-level date filter popoverDataTableAside- Sidebar componentDataTableSelectionBar- Bulk actions barDataTableEmptyState- Empty state composition components
Core filter implementation components that accept a table prop directly. They are used internally by the context-aware components but can also be used standalone when building custom components.
Components:
TableSearchFilter- Core search filterTablePagination- Core paginationTableFilterMenu- Core filter menuTableFacetedFilter- Core faceted filterTableSliderFilter- Core slider filterTableDateFilter- Core date filterTableSortMenu- Core sort menuTableViewMenu- Core view menuTableInlineFilter- Core inline filterTableClearFilter- Core clear filterTableExportButton- Core export buttonTableRangeFilter- Core range filter
Custom React hooks for table functionality.
Hooks:
useDataTable- Access table instance and contextuseDebounce- Debounce values for search/filtersuseDerivedColumnTitle- Derive column titlesuseGeneratedOptions- Generate filter options from datauseKeyboardShortcut- Manage keyboard shortcuts
Credits and Inspirations
Section titled “Credits and Inspirations”Niko Table is built on top of excellent open-source projects and inspired by the work of talented developers in the community.
Core Dependencies
Section titled “Core Dependencies”-
TanStack Table by Tanner Linsley - The headless table library that powers everything. Provides the foundation for all table functionality including sorting, filtering, pagination, and more.
-
Shadcn UI by Shadcn - Beautiful, accessible component primitives built on Radix UI. All UI components in Niko Table are built using Shadcn UI components.
Major Inspirations
Section titled “Major Inspirations”-
sadmann7’s work - Major inspiration for filter components and table patterns:
- TableCN - Inspired our filter menu, inline filter, faceted filter, and slider filter implementations. The composition pattern and filter architecture drew heavily from this excellent project.
- DiceUI Sortable - Drag and drop sortable for row reordering, which inspired the sort menu implementation.
-
nuqs by François Best - Type-safe search params state manager for URL state management. Used in server-side examples for managing table state in URLs.
-
Web Dev Simplified Registry by Kyle Cook - Registry implementation pattern that inspired the structure and organization of this project.
Philosophy
Section titled “Philosophy”Following the Shadcn philosophy: “Nobody’s table, everyone’s solution.”
- Copy and paste the code into your project
- Own the code - modify it as needed
- No dependencies on external packages (except TanStack Table and Shadcn UI)
- Fully customizable and themeable
- Built with TypeScript for type safety
License
Section titled “License”MIT License - use it freely in your projects!