Shadcn-Compatible React Data Table
Build production-ready data tables with sorting, filtering, pagination, virtualization, and more.
Nobody’s table, everyone’s solution.
This registry is not an opaque npm package — you copy DataTable source into your project and own it. Built with TanStack Table and shadcn/ui.
Add the registry once under registries in components.json, then install:
1{2 "registries": {3 "@niko-table": "https://niko-table.com/r/{name}.json"4 }5}pnpm dlx shadcn@latest add @niko-table/data-tableWorks with both shadcn generations — the classic Radix style (new-york) and the newer Base UI style (base-nova). See Installation for merging into an existing components.json and every available block.
Live Demo
Section titled “Live Demo”Product Name | Category | Brand | Price | Stock | Rating | In Stock | Release Date | Actions | ||
|---|---|---|---|---|---|---|---|---|---|---|
iPhone 15 Pro | Electronics | apple | $999.00 | 45 | 5★ | Yes | 7/10/2026 | |||
Galaxy S24 Ultra | Electronics | samsung | $1199.00 | 32 | 5★ | Yes | 7/5/2026 | |||
Air Jordan 1 | Sports | nike | $170.00 | 8 | 4★ | Yes | 6/20/2026 | |||
Ultraboost 23 | Sports | adidas | $190.00 | 15 | 4★ | Yes | 5/26/2026 | |||
PlayStation 5 | Electronics | sony | $499.00 | 0 | 5★ | No | 7/15/2025 | |||
OLED C3 TV | Electronics | lg | $1499.00 | 12 | 5★ | Yes | 4/16/2026 | |||
XPS 15 Laptop | Electronics | dell | $1899.00 | 20 | 4★ | Yes | 3/17/2026 | |||
Spectre x360 | Electronics | hp | $1599.00 | 18 | 4★ | Yes | 6/30/2026 | |||
MacBook Pro 16 | Electronics | apple | $2499.00 | 25 | 5★ | Yes | 6/15/2026 | |||
Galaxy Book3 | Electronics | samsung | $1399.00 | 14 | 4★ | Yes | 1/16/2026 |
Current Table State
Live view of all table state for demonstration
Search Query:None
Total Items:15
Selected Rows:0
Expanded Rows:0
Active Filters:0
Enhanced Filters:0
Active Enhanced:0
Join Logic:and
Sorting:None
Page:1 (Size: 10)
Hidden Columns:0
Pinned Columns:0 Left, 0 Right
View Full State Object
Enhanced Filters:
No enhanced filters
Column Pinning:
{
"start": [],
"end": []
}Filter Stats:
{
"totalFilters": 0,
"hasAndFilters": false,
"hasOrFilters": false,
"effectiveJoinOperator": "and",
"activeFilters": 0
}Filter Mode: AND
All conditions must match (stored in columnFilters)
Sorting:
[]
Column Filters State (AND logic):
[]
Global Filter State (OR logic):
""
Column Visibility:
{}Row Selection:
{}Expanded Rows:
{}1"use client"2
3/**4 * All Features Table Example5 *6 * This example demonstrates ALL available features of the DataTable:7 * - Multi-column sorting8 * - Advanced filtering (global search + column filters with AND/OR logic)9 * - Pagination10 * - Row selection with bulk actions11 * - Column visibility12 * - Row expansion13 * - Sidebar panels (left for filters, right for details)14 * - Data export (CSV)15 * - Controlled state management16 * - Selection bar with bulk actions17 */18
19import { useState, useCallback, useMemo } from "react"20import type {21 PaginationState,22 SortingState,23 ColumnFiltersState,24 ColumnVisibilityState,25 RowSelectionState,26 ExpandedState,27 ColumnPinningState,28} from "@tanstack/react-table"29import { DataTableRoot } from "@/components/niko-table/core/data-table-root"30import { DataTable } from "@/components/niko-table/core/data-table"31import {32 DataTableHeader,33 DataTableBody,34 DataTableEmptyBody,35} from "@/components/niko-table/core/data-table-structure"36import {37 DataTableAside,38 DataTableAsideContent,39 DataTableAsideHeader,40 DataTableAsideTitle,41 DataTableAsideDescription,42 DataTableAsideClose,43} from "@/components/niko-table/components/data-table-aside"44import { DataTableClearFilter } from "@/components/niko-table/components/data-table-clear-filter"45import { DataTableColumnActions } from "@/components/niko-table/components/data-table-column-actions"46import { DataTableColumnDateFilterOptions } from "@/components/niko-table/components/data-table-column-date-filter-options"47import { DataTableColumnFacetedFilterOptions } from "@/components/niko-table/components/data-table-column-faceted-filter"48import { DataTableColumnHeader } from "@/components/niko-table/components/data-table-column-header"49import { DataTableColumnTitle } from "@/components/niko-table/components/data-table-column-title"50import { DataTableColumnHideOptions } from "@/components/niko-table/components/data-table-column-hide"51import { DataTableColumnPinOptions } from "@/components/niko-table/components/data-table-column-pin"52import { DataTableColumnSliderFilterOptions } from "@/components/niko-table/components/data-table-column-slider-filter-options"53import { DataTableColumnSortOptions } from "@/components/niko-table/components/data-table-column-sort"54import {55 DataTableEmptyIcon,56 DataTableEmptyMessage,57 DataTableEmptyFilteredMessage,58 DataTableEmptyTitle,59 DataTableEmptyDescription,60 DataTableEmptyActions,61} from "@/components/niko-table/components/data-table-empty-state"62import { DataTableFacetedFilter } from "@/components/niko-table/components/data-table-faceted-filter"63import { DataTableFilterMenu } from "@/components/niko-table/components/data-table-filter-menu"64import { DataTablePagination } from "@/components/niko-table/components/data-table-pagination"65import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"66import { DataTableSelectionBar } from "@/components/niko-table/components/data-table-selection-bar"67import { DataTableSliderFilter } from "@/components/niko-table/components/data-table-slider-filter"68import { DataTableSortMenu } from "@/components/niko-table/components/data-table-sort-menu"69import { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"70import { DataTableViewMenu } from "@/components/niko-table/components/data-table-view-menu"71import {72 SYSTEM_COLUMN_IDS,73 FILTER_VARIANTS,74 JOIN_OPERATORS,75} from "@/components/niko-table/lib/constants"76import { useDataTable } from "@/components/niko-table/core/data-table-context"77import { daysAgo } from "@/components/niko-table/lib/format"78import { exportTableToCSV } from "@/components/niko-table/filters/table-export-button"79import type {80 DataTableColumnDef,81 ExtendedColumnFilter,82} from "@/components/niko-table/types"83import { Badge } from "@/components/ui/badge"84import { Button } from "@/components/ui/button"85import { Checkbox } from "@/components/ui/checkbox"86import { SearchX, UserSearch } from "lucide-react"87import {88 Card,89 CardAction,90 CardContent,91 CardDescription,92 CardHeader,93 CardTitle,94} from "@/components/ui/card"95import { ScrollArea } from "@/components/ui/scroll-area"96import { Separator } from "@/components/ui/separator"97import {98 Download,99 Trash2,100 ChevronRight,101 ChevronDown,102 MoreHorizontal,103} from "lucide-react"104import {105 DropdownMenu,106 DropdownMenuContent,107 DropdownMenuItem,108 DropdownMenuTrigger,109} from "@/components/ui/dropdown-menu"110
111type Product = {112 id: string113 name: string114 category: string115 brand: string116 price: number117 stock: number118 rating: number119 inStock: boolean120 releaseDate: Date121 description: string122 tags: string[]123}124
125const categoryOptions = [126 { label: "Electronics", value: "electronics" },127 { label: "Clothing", value: "clothing" },128 { label: "Home & Garden", value: "home-garden" },129 { label: "Sports", value: "sports" },130 { label: "Books", value: "books" },131]132
133const brandOptions = [134 { label: "Apple", value: "apple" },135 { label: "Samsung", value: "samsung" },136 { label: "Nike", value: "nike" },137 { label: "Adidas", value: "adidas" },138 { label: "Sony", value: "sony" },139 { label: "LG", value: "lg" },140 { label: "Dell", value: "dell" },141 { label: "HP", value: "hp" },142]143
144const initialData: Product[] = [145 {146 id: "1",147 name: "iPhone 15 Pro",148 category: "electronics",149 brand: "apple",150 price: 999,151 stock: 45,152 rating: 5,153 inStock: true,154 releaseDate: daysAgo(5),155 description: "Latest iPhone with A17 Pro chip and titanium design",156 tags: ["premium", "new", "smartphone"],157 },158 {159 id: "2",160 name: "Galaxy S24 Ultra",161 category: "electronics",162 brand: "samsung",163 price: 1199,164 stock: 32,165 rating: 5,166 inStock: true,167 releaseDate: daysAgo(10),168 description: "Flagship Android phone with S Pen and AI features",169 tags: ["premium", "new", "smartphone"],170 },171 {172 id: "3",173 name: "Air Jordan 1",174 category: "sports",175 brand: "nike",176 price: 170,177 stock: 8,178 rating: 4,179 inStock: true,180 releaseDate: daysAgo(25),181 description: "Classic basketball sneakers with iconic design",182 tags: ["sneakers", "basketball", "classic"],183 },184 {185 id: "4",186 name: "Ultraboost 23",187 category: "sports",188 brand: "adidas",189 price: 190,190 stock: 15,191 rating: 4,192 inStock: true,193 releaseDate: daysAgo(50),194 description: "Running shoes with Boost technology",195 tags: ["running", "comfort", "athletic"],196 },197 {198 id: "5",199 name: "PlayStation 5",200 category: "electronics",201 brand: "sony",202 price: 499,203 stock: 0,204 rating: 5,205 inStock: false,206 releaseDate: daysAgo(365),207 description: "Next-gen gaming console with ray tracing",208 tags: ["gaming", "console", "entertainment"],209 },210 {211 id: "6",212 name: "OLED C3 TV",213 category: "electronics",214 brand: "lg",215 price: 1499,216 stock: 12,217 rating: 5,218 inStock: true,219 releaseDate: daysAgo(90),220 description: "55-inch OLED TV with perfect blacks",221 tags: ["tv", "entertainment", "premium"],222 },223 {224 id: "7",225 name: "XPS 15 Laptop",226 category: "electronics",227 brand: "dell",228 price: 1899,229 stock: 20,230 rating: 4,231 inStock: true,232 releaseDate: daysAgo(120),233 description: "Premium laptop for professionals",234 tags: ["laptop", "professional", "premium"],235 },236 {237 id: "8",238 name: "Spectre x360",239 category: "electronics",240 brand: "hp",241 price: 1599,242 stock: 18,243 rating: 4,244 inStock: true,245 releaseDate: daysAgo(15),246 description: "2-in-1 convertible laptop",247 tags: ["laptop", "convertible", "versatile"],248 },249 {250 id: "9",251 name: "MacBook Pro 16",252 category: "electronics",253 brand: "apple",254 price: 2499,255 stock: 25,256 rating: 5,257 inStock: true,258 releaseDate: daysAgo(30),259 description: "Powerful laptop for creative professionals",260 tags: ["laptop", "professional", "creative"],261 },262 {263 id: "10",264 name: "Galaxy Book3",265 category: "electronics",266 brand: "samsung",267 price: 1399,268 stock: 14,269 rating: 4,270 inStock: true,271 releaseDate: daysAgo(180),272 description: "Sleek Windows laptop",273 tags: ["laptop", "windows", "sleek"],274 },275 {276 id: "11",277 name: "Running Shorts",278 category: "clothing",279 brand: "nike",280 price: 45,281 stock: 120,282 rating: 3,283 inStock: true,284 releaseDate: daysAgo(60),285 description: "Comfortable running shorts",286 tags: ["clothing", "running", "athletic"],287 },288 {289 id: "12",290 name: "Training Jacket",291 category: "clothing",292 brand: "adidas",293 price: 85,294 stock: 65,295 rating: 4,296 inStock: true,297 releaseDate: daysAgo(45),298 description: "Lightweight training jacket",299 tags: ["clothing", "training", "athletic"],300 },301 {302 id: "13",303 name: "Garden Tools Set",304 category: "home-garden",305 brand: "hp",306 price: 120,307 stock: 30,308 rating: 4,309 inStock: true,310 releaseDate: daysAgo(75),311 description: "Complete set of gardening tools",312 tags: ["tools", "garden", "home"],313 },314 {315 id: "14",316 name: "Programming Book",317 category: "books",318 brand: "dell",319 price: 60,320 stock: 50,321 rating: 5,322 inStock: true,323 releaseDate: daysAgo(200),324 description: "Learn React and TypeScript",325 tags: ["book", "programming", "education"],326 },327 {328 id: "15",329 name: "Wireless Mouse",330 category: "electronics",331 brand: "lg",332 price: 35,333 stock: 200,334 rating: 3,335 inStock: true,336 releaseDate: daysAgo(150),337 description: "Ergonomic wireless mouse",338 tags: ["accessories", "computer", "wireless"],339 },340]341
342// Expanded row content component343function ExpandedRowContent({ product }: { product: Product }) {344 return (345 <div className="bg-muted/30 p-4">346 <div className="space-y-3">347 <div>348 <h4 className="mb-2 text-sm font-semibold">Description</h4>349 <p className="text-sm text-muted-foreground">{product.description}</p>350 </div>351 <div>352 <h4 className="mb-2 text-sm font-semibold">Tags</h4>353 <div className="flex flex-wrap gap-2">354 {product.tags.map(tag => (355 <Badge key={tag} variant="secondary" className="text-xs">356 {tag}357 </Badge>358 ))}359 </div>360 </div>361 </div>362 </div>363 )364}365
366// Product details component for sidebar367function ProductDetails({ product }: { product: Product }) {368 return (369 <ScrollArea className="h-full">370 <div className="space-y-6 p-6">371 <div>372 <h2 className="text-2xl font-bold">{product.name}</h2>373 <p className="mt-1 text-sm text-muted-foreground">374 {categoryOptions.find(opt => opt.value === product.category)?.label}375 </p>376 </div>377
378 <Separator />379
380 <div className="space-y-4">381 <div>382 <h3 className="mb-2 text-sm font-semibold">Details</h3>383 <div className="space-y-2 text-sm">384 <div className="flex justify-between">385 <span className="text-muted-foreground">Brand:</span>386 <span>387 {brandOptions.find(opt => opt.value === product.brand)?.label}388 </span>389 </div>390 <div className="flex justify-between">391 <span className="text-muted-foreground">Price:</span>392 <span className="font-medium">${product.price.toFixed(2)}</span>393 </div>394 <div className="flex justify-between">395 <span className="text-muted-foreground">Stock:</span>396 <span397 className={398 product.stock < 10 ? "font-medium text-red-600" : ""399 }400 >401 {product.stock} units402 </span>403 </div>404 <div className="flex justify-between">405 <span className="text-muted-foreground">Rating:</span>406 <div className="flex items-center gap-1">407 <span>{product.rating}</span>408 <span className="text-yellow-500">★</span>409 </div>410 </div>411 <div className="flex justify-between">412 <span className="text-muted-foreground">Status:</span>413 <Badge variant={product.inStock ? "default" : "secondary"}>414 {product.inStock ? "In Stock" : "Out of Stock"}415 </Badge>416 </div>417 <div className="flex justify-between">418 <span className="text-muted-foreground">Release Date:</span>419 <span>{product.releaseDate.toLocaleDateString("en-US")}</span>420 </div>421 </div>422 </div>423
424 <Separator />425
426 <div>427 <h3 className="mb-2 text-sm font-semibold">Description</h3>428 <p className="text-sm text-muted-foreground">429 {product.description}430 </p>431 </div>432
433 <Separator />434
435 <div>436 <h3 className="mb-2 text-sm font-semibold">Tags</h3>437 <div className="flex flex-wrap gap-2">438 {product.tags.map(tag => (439 <Badge key={tag} variant="outline" className="text-xs">440 {tag}441 </Badge>442 ))}443 </div>444 </div>445 </div>446 </div>447 </ScrollArea>448 )449}450
451// Bulk actions component452function BulkActions() {453 const { table } = useDataTable<Product>()454 const selectedRows = table.getFilteredSelectedRowModel().rows455 const selectedCount = selectedRows.length456
457 const handleBulkExport = () => {458 exportTableToCSV(table, {459 filename: "selected-products",460 excludeColumns: [461 "select",462 "expand",463 "actions",464 ] as unknown as (keyof Product)[],465 onlySelected: true,466 })467 }468
469 const handleBulkDelete = () => {470 // In a real app, you would delete the selected items471 console.log(472 "Deleting:",473 selectedRows.map(row => row.original.id),474 )475 table.resetRowSelection()476 }477
478 return (479 <DataTableSelectionBar480 selectedCount={selectedCount}481 onClear={() => table.resetRowSelection()}482 >483 <Button size="sm" variant="outline" onClick={handleBulkExport}>484 <Download className="mr-2 h-4 w-4" />485 Export Selected486 </Button>487 <Button size="sm" variant="destructive" onClick={handleBulkDelete}>488 <Trash2 className="mr-2 h-4 w-4" />489 Delete Selected490 </Button>491 </DataTableSelectionBar>492 )493}494
495// Filter toolbar component496function FilterToolbar({497 filters,498 onFiltersChange,499}: {500 filters: ExtendedColumnFilter<Product>[]501 onFiltersChange: (filters: ExtendedColumnFilter<Product>[] | null) => void502}) {503 return (504 <DataTableToolbarSection className="w-full flex-col justify-between gap-2">505 <DataTableToolbarSection className="px-0">506 <DataTableSearchFilter placeholder="Search products..." />507 <DataTableViewMenu />508 </DataTableToolbarSection>509 <DataTableToolbarSection className="flex-wrap px-0">510 <DataTableFacetedFilter511 accessorKey="category"512 title="Category"513 options={categoryOptions}514 limitToFilteredRows515 multiple516 />517 <DataTableFacetedFilter518 accessorKey="brand"519 title="Brand"520 options={brandOptions}521 limitToFilteredRows522 multiple523 />524 <DataTableSliderFilter accessorKey="price" />525 <DataTableSortMenu />526 <DataTableFilterMenu527 filters={filters}528 onFiltersChange={onFiltersChange}529 />530 <DataTableClearFilter />531 </DataTableToolbarSection>532 </DataTableToolbarSection>533 )534}535
536export default function AllFeaturesTableExample() {537 // Controlled state management538 const [data] = useState<Product[]>(initialData)539 const [globalFilter, setGlobalFilter] = useState<string | object>("")540 const [sorting, setSorting] = useState<SortingState>([])541 const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])542 const [columnVisibility, setColumnVisibility] =543 useState<ColumnVisibilityState>({})544 const [rowSelection, setRowSelection] = useState<RowSelectionState>({})545 const [expanded, setExpanded] = useState<ExpandedState>({})546 const [pagination, setPagination] = useState<PaginationState>({547 pageIndex: 0,548 pageSize: 10,549 })550 const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({551 start: [],552 end: [],553 })554
555 // Sidebar state556 const [selectedProductId, setSelectedProductId] = useState<string | null>(557 null,558 )559
560 const selectedProduct = selectedProductId561 ? data.find(product => product.id === selectedProductId)562 : null563
564 const resetAllState = useCallback(() => {565 setGlobalFilter("")566 setSorting([])567 setColumnFilters([])568 setColumnVisibility({})569 setRowSelection({})570 setExpanded({})571 setColumnPinning({ start: [], end: [] })572 setPagination({ pageIndex: 0, pageSize: 10 })573 setSelectedProductId(null)574 }, [])575
576 // Extract filters for display577 const currentFilters = useMemo(() => {578 if (579 typeof globalFilter === "object" &&580 globalFilter &&581 "filters" in globalFilter582 ) {583 const filterObj = globalFilter as {584 filters: ExtendedColumnFilter<Product>[]585 }586 return filterObj.filters || []587 }588 return columnFilters589 .map(cf => cf.value)590 .filter(591 (v): v is ExtendedColumnFilter<Product> =>592 v !== null && typeof v === "object" && "id" in v,593 )594 }, [globalFilter, columnFilters])595
596 // Handler for filter menu597 const handleFiltersChange = useCallback(598 (filters: ExtendedColumnFilter<Product>[] | null) => {599 if (!filters || filters.length === 0) {600 setColumnFilters([])601 setGlobalFilter("")602 setPagination(prev => ({ ...prev, pageIndex: 0 }))603 } else {604 const hasOrFilters = filters.some(605 (filter, index) => index > 0 && filter.joinOperator === "or",606 )607 if (hasOrFilters) {608 setColumnFilters([])609 setGlobalFilter({610 filters,611 joinOperator: "mixed",612 })613 setPagination(prev => ({ ...prev, pageIndex: 0 }))614 } else {615 setGlobalFilter("")616 setColumnFilters(617 filters.map(filter => ({618 id: filter.id,619 value: filter,620 })),621 )622 setPagination(prev => ({ ...prev, pageIndex: 0 }))623 }624 }625 },626 [],627 )628
629 // Helper to display global filter state630 const getGlobalFilterDisplay = () => {631 if (typeof globalFilter === "string") {632 return globalFilter || "None"633 }634 if (635 typeof globalFilter === "object" &&636 globalFilter &&637 "filters" in globalFilter638 ) {639 const filterObj = globalFilter as {640 filters: unknown[]641 joinOperator: string642 }643 return `OR Filter (${filterObj.filters?.length || 0} conditions)`644 }645 return "None"646 }647
648 // Extract actual filter data for display649 const displayFilters = useMemo(() => {650 if (651 typeof globalFilter === "object" &&652 globalFilter &&653 "filters" in globalFilter654 ) {655 const filterObj = globalFilter as {656 filters: unknown[]657 joinOperator: string658 }659 return filterObj.filters || []660 }661 return columnFilters662 }, [columnFilters, globalFilter])663
664 // Enhanced filter statistics665 const filterStats = useMemo(() => {666 if (667 typeof globalFilter === "object" &&668 globalFilter &&669 "filters" in globalFilter670 ) {671 const filterObj = globalFilter as {672 filters: Array<{673 joinOperator?: string674 value?: unknown675 }>676 joinOperator: string677 }678 const filters = filterObj.filters || []679
680 const hasAndFilters = filters.some(681 (filter, index) =>682 index === 0 || filter.joinOperator === JOIN_OPERATORS.AND,683 )684 const hasOrFilters = filters.some(685 (filter, index) =>686 index > 0 && filter.joinOperator === JOIN_OPERATORS.OR,687 )688
689 return {690 totalFilters: filters.length,691 hasAndFilters,692 hasOrFilters,693 effectiveJoinOperator: hasOrFilters694 ? JOIN_OPERATORS.MIXED695 : JOIN_OPERATORS.AND,696 activeFilters: filters.filter(f => f.value && f.value !== "").length,697 }698 }699
700 const hasAndFilters = columnFilters.length > 0701 const hasOrFilters = columnFilters.some(702 filter =>703 typeof filter.value === "object" &&704 filter.value &&705 "joinOperator" in filter.value &&706 filter.value.joinOperator === "or",707 )708
709 return {710 totalFilters: columnFilters.length,711 hasAndFilters,712 hasOrFilters,713 effectiveJoinOperator: hasOrFilters714 ? JOIN_OPERATORS.MIXED715 : JOIN_OPERATORS.AND,716 activeFilters: columnFilters.filter(f => f.value && f.value !== "")717 .length,718 }719 }, [columnFilters, globalFilter])720
721 // Get current filter mode722 const getFilterMode = () => {723 if (724 typeof globalFilter === "object" &&725 globalFilter &&726 "filters" in globalFilter727 ) {728 const filterObj = globalFilter as {729 filters: unknown[]730 joinOperator: string731 }732 if (filterObj.joinOperator === "mixed") {733 return "MIXED"734 }735 return filterObj.joinOperator.toUpperCase()736 }737
738 const hasOrOperators = columnFilters.some(739 filter =>740 typeof filter.value === "object" &&741 filter.value &&742 "joinOperator" in filter.value &&743 filter.value.joinOperator === "or",744 )745
746 return hasOrOperators ? "MIXED" : "AND"747 }748
749 // Define columns with all features750 const columns: DataTableColumnDef<Product>[] = useMemo(751 () => [752 {753 id: SYSTEM_COLUMN_IDS.SELECT,754 size: 40, // Compact width for checkbox column755 header: ({ table }) => (756 <Checkbox757 checked={758 table.getIsAllPageRowsSelected() ||759 (table.getIsSomePageRowsSelected() && "indeterminate")760 }761 onCheckedChange={value => table.toggleAllPageRowsSelected(!!value)}762 aria-label="Select all"763 />764 ),765 cell: ({ row }) => (766 <Checkbox767 checked={row.getIsSelected()}768 onCheckedChange={value => row.toggleSelected(!!value)}769 aria-label="Select row"770 />771 ),772 enableSorting: false,773 enableHiding: false,774 },775 {776 id: SYSTEM_COLUMN_IDS.EXPAND,777 header: () => null,778 cell: ({ row }) => {779 if (!row.getCanExpand()) return null780 return (781 <Button782 variant="ghost"783 size="sm"784 className="h-6 w-6 p-0"785 onClick={row.getToggleExpandedHandler()}786 >787 {row.getIsExpanded() ? (788 <ChevronDown className="h-4 w-4" />789 ) : (790 <ChevronRight className="h-4 w-4" />791 )}792 </Button>793 )794 },795 size: 50,796 enableSorting: false,797 enableHiding: false,798 meta: {799 expandedContent: (product: Product) => (800 <ExpandedRowContent product={product} />801 ),802 },803 },804 {805 accessorKey: "name",806 header: () => (807 <DataTableColumnHeader className="justify-start">808 <DataTableColumnTitle>Product Name</DataTableColumnTitle>809 <DataTableColumnActions>810 <DataTableColumnSortOptions withSeparator={false} />811 <DataTableColumnPinOptions />812 <DataTableColumnHideOptions />813 </DataTableColumnActions>814 </DataTableColumnHeader>815 ),816 meta: {817 label: "Product Name",818 variant: FILTER_VARIANTS.TEXT,819 },820 enableColumnFilter: true,821 cell: ({ row }) => (822 <div823 className="cursor-pointer font-medium hover:underline"824 onClick={() => {825 setSelectedProductId(row.original.id)826 }}827 >828 {row.getValue("name")}829 </div>830 ),831 },832 {833 accessorKey: "category",834 header: () => (835 <DataTableColumnHeader>836 <DataTableColumnTitle />837 {/* Composable Actions: Multi-select filter example */}838 <DataTableColumnActions label="Category Options">839 <DataTableColumnSortOptions840 variant={FILTER_VARIANTS.TEXT}841 withSeparator={false}842 />843 <DataTableColumnFacetedFilterOptions844 options={categoryOptions}845 multiple846 />847 <DataTableColumnPinOptions />848 <DataTableColumnHideOptions />849 </DataTableColumnActions>850 </DataTableColumnHeader>851 ),852 meta: {853 label: "Category",854 variant: FILTER_VARIANTS.SELECT,855 options: categoryOptions,856 },857 cell: ({ row }) => {858 const category = row.getValue("category") as string859 const option = categoryOptions.find(opt => opt.value === category)860 return <span>{option?.label || category}</span>861 },862 enableColumnFilter: true,863 },864 {865 accessorKey: "brand",866 header: () => (867 <DataTableColumnHeader>868 <DataTableColumnTitle />869 {/* Composable Actions: Single-select filter example */}870 <DataTableColumnActions label="Brand Options">871 <DataTableColumnSortOptions872 variant={FILTER_VARIANTS.TEXT}873 withSeparator={false}874 />875 <DataTableColumnFacetedFilterOptions876 options={brandOptions}877 multiple={false}878 />879 <DataTableColumnPinOptions />880 <DataTableColumnHideOptions />881 </DataTableColumnActions>882 </DataTableColumnHeader>883 ),884 meta: {885 label: "Brand",886 variant: FILTER_VARIANTS.SELECT,887 options: brandOptions,888 },889 enableColumnFilter: true,890 },891 {892 accessorKey: "price",893 header: () => (894 <DataTableColumnHeader>895 <DataTableColumnTitle />896 <DataTableColumnActions>897 <DataTableColumnSortOptions withSeparator={false} />898 <DataTableColumnSliderFilterOptions />899 <DataTableColumnPinOptions />900 <DataTableColumnHideOptions />901 </DataTableColumnActions>902 </DataTableColumnHeader>903 ),904 meta: {905 label: "Price",906 unit: "$",907 variant: FILTER_VARIANTS.RANGE,908 },909 cell: ({ row }) => {910 const price = parseFloat(row.getValue("price"))911 return <div className="font-medium">${price.toFixed(2)}</div>912 },913 enableColumnFilter: true,914 },915 {916 accessorKey: "stock",917 header: () => (918 <DataTableColumnHeader>919 <DataTableColumnTitle />920 {/* All actions composed in single dropdown */}921 <DataTableColumnActions>922 <DataTableColumnSortOptions923 variant={FILTER_VARIANTS.NUMBER}924 withSeparator={false}925 />926 <DataTableColumnPinOptions />927 <DataTableColumnHideOptions />928 </DataTableColumnActions>929 </DataTableColumnHeader>930 ),931 meta: {932 label: "Stock",933 variant: FILTER_VARIANTS.NUMBER,934 },935 cell: ({ row }) => {936 const stock = Number(row.getValue("stock"))937 return (938 <div className={stock < 10 ? "font-medium text-red-600" : ""}>939 {stock}940 </div>941 )942 },943 enableColumnFilter: true,944 },945 {946 accessorKey: "rating",947 header: () => (948 <DataTableColumnHeader>949 <DataTableColumnTitle />950 <DataTableColumnActions>951 <DataTableColumnSortOptions952 variant={FILTER_VARIANTS.NUMBER}953 withSeparator={false}954 />955 <DataTableColumnPinOptions />956 <DataTableColumnHideOptions />957 </DataTableColumnActions>958 </DataTableColumnHeader>959 ),960 meta: {961 label: "Rating",962 variant: FILTER_VARIANTS.NUMBER,963 },964 cell: ({ row }) => {965 const rating = Number(row.getValue("rating"))966 return (967 <div className="flex items-center gap-1">968 <span>{rating}</span>969 <span className="text-yellow-500">★</span>970 </div>971 )972 },973 enableColumnFilter: true,974 },975 {976 accessorKey: "inStock",977 header: () => (978 <DataTableColumnHeader>979 <DataTableColumnTitle />980 <DataTableColumnActions>981 <DataTableColumnSortOptions withSeparator={false} />982 <DataTableColumnPinOptions />983 <DataTableColumnHideOptions />984 </DataTableColumnActions>985 </DataTableColumnHeader>986 ),987 meta: {988 label: "In Stock",989 variant: FILTER_VARIANTS.BOOLEAN,990 },991 cell: ({ row }) => {992 const inStock = Boolean(row.getValue("inStock"))993 return (994 <Badge variant={inStock ? "default" : "secondary"}>995 {inStock ? "Yes" : "No"}996 </Badge>997 )998 },999 enableColumnFilter: true,1000 },1001 {1002 accessorKey: "releaseDate",1003 header: () => (1004 <DataTableColumnHeader>1005 <DataTableColumnTitle />1006 <DataTableColumnActions>1007 <DataTableColumnSortOptions withSeparator={false} />1008 <DataTableColumnDateFilterOptions />1009 <DataTableColumnPinOptions />1010 <DataTableColumnHideOptions />1011 </DataTableColumnActions>1012 </DataTableColumnHeader>1013 ),1014 meta: {1015 label: "Release Date",1016 variant: FILTER_VARIANTS.DATE,1017 },1018 cell: ({ row }) => {1019 const date = row.getValue("releaseDate") as Date1020 return <span>{date.toLocaleDateString("en-US")}</span>1021 },1022 enableColumnFilter: true,1023 },1024 {1025 id: "actions",1026 header: () => <div className="text-right">Actions</div>,1027 cell: ({ row }) => {1028 const product = row.original1029 return (1030 <div className="flex justify-end">1031 <DropdownMenu>1032 <DropdownMenuTrigger asChild>1033 <Button variant="ghost" className="h-8 w-8 p-0">1034 <MoreHorizontal className="h-4 w-4" />1035 </Button>1036 </DropdownMenuTrigger>1037 <DropdownMenuContent align="end">1038 <DropdownMenuItem1039 onClick={() => {1040 setSelectedProductId(product.id)1041 }}1042 >1043 View Details1044 </DropdownMenuItem>1045 <DropdownMenuItem1046 onClick={() => console.log("Edit", product.id)}1047 >1048 Edit1049 </DropdownMenuItem>1050 <DropdownMenuItem1051 onClick={() => console.log("Delete", product.id)}1052 className="text-red-600"1053 >1054 Delete1055 </DropdownMenuItem>1056 </DropdownMenuContent>1057 </DropdownMenu>1058 </div>1059 )1060 },1061 enableSorting: false,1062 enableHiding: false,1063 },1064 ],1065 [],1066 )1067
1068 return (1069 <div className="w-full space-y-4">1070 <DataTableRoot1071 data={data}1072 columns={columns}1073 config={{1074 enablePagination: true,1075 enableSorting: true,1076 enableMultiSort: true,1077 enableFilters: true,1078 enableRowSelection: true,1079 enableExpanding: true,1080 }}1081 getRowCanExpand={() => true}1082 getSubRows={() => undefined}1083 state={{1084 globalFilter,1085 sorting,1086 columnFilters,1087 columnVisibility,1088 rowSelection,1089 expanded,1090 columnPinning,1091 pagination,1092 }}1093 onGlobalFilterChange={value => {1094 setGlobalFilter(value)1095 setPagination(prev => ({ ...prev, pageIndex: 0 }))1096 }}1097 onSortingChange={setSorting}1098 onColumnFiltersChange={setColumnFilters}1099 onColumnVisibilityChange={setColumnVisibility}1100 onRowSelectionChange={setRowSelection}1101 onExpandedChange={setExpanded}1102 onColumnPinningChange={setColumnPinning}1103 onPaginationChange={setPagination}1104 >1105 <FilterToolbar1106 filters={currentFilters}1107 onFiltersChange={handleFiltersChange}1108 />1109 <BulkActions />1110
1111 {/* Sidebar Layout */}1112 <div className="flex min-h-150 gap-4">1113 {/* Main Table Area */}1114 <DataTable className="flex-1" height="100%">1115 <DataTableHeader />1116 <DataTableBody1117 onRowClick={(product: Product) => {1118 console.log("Row clicked:", product.id)1119 setSelectedProductId(product.id)1120 }}1121 >1122 <DataTableEmptyBody>1123 <DataTableEmptyMessage>1124 <DataTableEmptyIcon>1125 <UserSearch className="size-12" />1126 </DataTableEmptyIcon>1127 <DataTableEmptyTitle>No products found</DataTableEmptyTitle>1128 <DataTableEmptyDescription>1129 Get started by adding your first product to the inventory.1130 </DataTableEmptyDescription>1131 </DataTableEmptyMessage>1132 <DataTableEmptyFilteredMessage>1133 <DataTableEmptyIcon>1134 <SearchX className="size-12" />1135 </DataTableEmptyIcon>1136 <DataTableEmptyTitle>No matches found</DataTableEmptyTitle>1137 <DataTableEmptyDescription>1138 Try adjusting your filters or search to find what1139 you're looking for.1140 </DataTableEmptyDescription>1141 </DataTableEmptyFilteredMessage>1142 <DataTableEmptyActions>1143 <Button onClick={() => alert("Add product clicked")}>1144 Add Product1145 </Button>1146 </DataTableEmptyActions>1147 </DataTableEmptyBody>1148 </DataTableBody>1149 </DataTable>1150
1151 {/* Right Sidebar - Product Details */}1152 {selectedProduct && (1153 <DataTableAside1154 side="right"1155 open={!!selectedProduct}1156 onOpenChange={open => {1157 if (!open) setSelectedProductId(null)1158 }}1159 >1160 <DataTableAsideContent width="w-78">1161 <DataTableAsideHeader>1162 <DataTableAsideTitle>Product Details</DataTableAsideTitle>1163 <DataTableAsideDescription>1164 View detailed information1165 </DataTableAsideDescription>1166 <DataTableAsideClose />1167 </DataTableAsideHeader>1168 <ProductDetails product={selectedProduct} />1169 </DataTableAsideContent>1170 </DataTableAside>1171 )}1172 </div>1173 <DataTablePagination />1174 </DataTableRoot>1175
1176 {/* State Display */}1177 <Card>1178 <CardHeader>1179 <CardTitle>Current Table State</CardTitle>1180 <CardDescription>1181 Live view of all table state for demonstration1182 </CardDescription>1183 <CardAction>1184 <Button variant="outline" size="sm" onClick={resetAllState}>1185 Reset All State1186 </Button>1187 </CardAction>1188 </CardHeader>1189 <CardContent className="space-y-4">1190 <div className="grid gap-2 text-xs text-muted-foreground">1191 <div className="flex justify-between">1192 <span className="font-medium">Search Query:</span>1193 <span className="text-foreground">1194 {getGlobalFilterDisplay()}1195 </span>1196 </div>1197
1198 <div className="flex justify-between">1199 <span className="font-medium">Total Items:</span>1200 <span className="text-foreground">{data.length}</span>1201 </div>1202
1203 <div className="flex justify-between">1204 <span className="font-medium">Selected Rows:</span>1205 <span className="text-foreground">1206 {1207 Object.keys(rowSelection).filter(key => rowSelection[key])1208 .length1209 }1210 </span>1211 </div>1212
1213 <div className="flex justify-between">1214 <span className="font-medium">Expanded Rows:</span>1215 <span className="text-foreground">1216 {typeof expanded === "object" && expanded !== null1217 ? Object.keys(expanded).filter(1218 key => (expanded as Record<string, boolean>)[key],1219 ).length1220 : 0}1221 </span>1222 </div>1223
1224 <div className="flex justify-between">1225 <span className="font-medium">Active Filters:</span>1226 <span className="text-foreground">{columnFilters.length}</span>1227 </div>1228
1229 <div className="flex justify-between">1230 <span className="font-medium">Enhanced Filters:</span>1231 <span className="text-foreground">1232 {filterStats.totalFilters}1233 </span>1234 </div>1235
1236 <div className="flex justify-between">1237 <span className="font-medium">Active Enhanced:</span>1238 <span className="text-foreground">1239 {filterStats.activeFilters}1240 </span>1241 </div>1242
1243 <div className="flex justify-between">1244 <span className="font-medium">Join Logic:</span>1245 <span className="text-foreground">1246 {filterStats.effectiveJoinOperator}1247 </span>1248 </div>1249
1250 <div className="flex justify-between">1251 <span className="font-medium">Sorting:</span>1252 <span className="text-foreground">1253 {sorting.length > 01254 ? sorting1255 .map(s => `${s.id} ${s.desc ? "desc" : "asc"}`)1256 .join(", ")1257 : "None"}1258 </span>1259 </div>1260
1261 <div className="flex justify-between">1262 <span className="font-medium">Page:</span>1263 <span className="text-foreground">1264 {pagination.pageIndex + 1} (Size: {pagination.pageSize})1265 </span>1266 </div>1267
1268 <div className="flex justify-between">1269 <span className="font-medium">Hidden Columns:</span>1270 <span className="text-foreground">1271 {1272 Object.values(columnVisibility).filter(v => v === false)1273 .length1274 }1275 </span>1276 </div>1277
1278 <div className="flex justify-between">1279 <span className="font-medium">Pinned Columns:</span>1280 <span className="text-foreground">1281 {columnPinning.start?.length || 0} Left,{" "}1282 {columnPinning.end?.length || 0} Right1283 </span>1284 </div>1285 </div>1286
1287 {/* Detailed state (collapsible) */}1288 <details className="border-t pt-4">1289 <summary className="cursor-pointer text-xs font-medium hover:text-foreground">1290 View Full State Object1291 </summary>1292 <div className="mt-4 space-y-3 text-xs">1293 <div>1294 <strong>Enhanced Filters:</strong>1295 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1296 {displayFilters.length > 01297 ? JSON.stringify(displayFilters, null, 2)1298 : "No enhanced filters"}1299 </pre>1300 </div>1301 <div>1302 <strong>Column Pinning:</strong>1303 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1304 {JSON.stringify(columnPinning, null, 2)}1305 </pre>1306 </div>1307 <div>1308 <strong>Filter Stats:</strong>1309 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1310 {JSON.stringify(filterStats, null, 2)}1311 </pre>1312 </div>1313 <div>1314 <strong>Filter Mode:</strong> {getFilterMode()}1315 <div className="mt-1 text-muted-foreground">1316 {getFilterMode() === "AND"1317 ? "All conditions must match (stored in columnFilters)"1318 : getFilterMode() === "OR"1319 ? "Any condition can match (stored in globalFilter)"1320 : "Mixed logic - individual AND/OR operators per filter"}1321 </div>1322 </div>1323 <div>1324 <strong>Sorting:</strong>1325 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1326 {JSON.stringify(sorting, null, 2)}1327 </pre>1328 </div>1329 <div>1330 <strong>Column Filters State (AND logic):</strong>1331 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1332 {JSON.stringify(columnFilters, null, 2)}1333 </pre>1334 </div>1335 <div>1336 <strong>Global Filter State (OR logic):</strong>1337 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1338 {JSON.stringify(globalFilter, null, 2)}1339 </pre>1340 </div>1341 <div>1342 <strong>Column Visibility:</strong>1343 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1344 {JSON.stringify(columnVisibility, null, 2)}1345 </pre>1346 </div>1347 <div>1348 <strong>Row Selection:</strong>1349 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1350 {JSON.stringify(rowSelection, null, 2)}1351 </pre>1352 </div>1353 <div>1354 <strong>Expanded Rows:</strong>1355 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1356 {JSON.stringify(expanded, null, 2)}1357 </pre>1358 </div>1359 </div>1360 </details>1361 </CardContent>1362 </Card>1363 </div>1364 )1365}Selection, expansion, filters, sort, pagination, column visibility, export, and asides — composed from registry pieces. Browse more in Examples.
Quick Links
Section titled “Quick Links”Getting Started
Section titled “Getting Started”- Introduction - Learn about the architecture and philosophy
- Installation - Set up your project
- Manual Installation - Copy components manually
Examples
Section titled “Examples”- Simple Table - Basic rendering
- Basic Table - Pagination and sorting
- Search Table - Add global search
- Faceted Filter Table - Column-specific filters
- Virtualization Table - 10,000+ rows with virtual scrolling
- Row Context Menu Table - Shared kebab and right-click actions
- Advanced Table - All features combined
- Advanced Nuqs Table - URL state persistence
- Server-Side Table - Pagination, sorting, and filters on the server
- Server-Side Nuqs Table - Server-side table with URL state
- Drizzle ORM - Server-side wire contract with Drizzle + Postgres
- Drizzle ORM + Nuqs - Drizzle backend with shareable URL state
Data Grid
Section titled “Data Grid”- Introduction - The composable, editable spreadsheet grid
- Cell Types - Text, number, currency, checkbox, date, and select editors
- Validation - Inline per-cell errors with Zod (or any
resolve) - Dynamic Columns - Add, rename, move, delete, and retype at runtime
- Persistence - Create / update / delete change-sets with
useGridChanges - API Reference - Every hook, component, and type
Key Features
Section titled “Key Features”- Type-safe - Full TypeScript support
- Accessible - shadcn/ui primitives (Radix or Base UI), ARIA labels in filters/menus, keyboard shortcuts; linted with jsx-a11y
- Responsive - Full-width scroll container (
overflow-auto); touch-friendly controls; stack toolbars as needed on small screens - Customizable - Full source code access
- Composable - Mix and match
DataTable*components (install only the registry pieces you need). TanStack Table v9 mirrors that at the engine: register only the features you use inlib/data-table-features.ts - Performance - Virtual scrolling for 10,000+ client-side rows (fixed-height scroll container)
- State Management - Context-based
useDataTable()with controlled state and optional URL sync (nuqs)
Built With
Section titled “Built With”- TanStack Table - Headless table utilities
- Shadcn UI - Beautiful UI components
- Tailwind CSS - Utility-first CSS
- Radix UI / Base UI - Accessible primitives (per your shadcn generation)
- DiceUI Sortable - Drag and drop sortable
Community
Section titled “Community”Have questions or want to contribute?
License
Section titled “License”MIT License - feel free to use this in your projects!