Shadcn-Compatible React Data Table
Build production-ready data tables with sorting, filtering, pagination, virtualization, and more.
Nobody’s table, everyone’s solution.
Niko Table is not an opaque npm package — you copy the 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:
{
"left": [],
"right": []
}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 VisibilityState,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()}</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] = useState<VisibilityState>({})543 const [rowSelection, setRowSelection] = useState<RowSelectionState>({})544 const [expanded, setExpanded] = useState<ExpandedState>({})545 const [pagination, setPagination] = useState<PaginationState>({546 pageIndex: 0,547 pageSize: 10,548 })549 const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({550 left: [],551 right: [],552 })553
554 // Sidebar state555 const [selectedProductId, setSelectedProductId] = useState<string | null>(556 null,557 )558
559 const selectedProduct = selectedProductId560 ? data.find(product => product.id === selectedProductId)561 : null562
563 const resetAllState = useCallback(() => {564 setGlobalFilter("")565 setSorting([])566 setColumnFilters([])567 setColumnVisibility({})568 setRowSelection({})569 setExpanded({})570 setColumnPinning({ left: [], right: [] })571 setPagination({ pageIndex: 0, pageSize: 10 })572 setSelectedProductId(null)573 }, [])574
575 // Extract filters for display576 const currentFilters = useMemo(() => {577 if (578 typeof globalFilter === "object" &&579 globalFilter &&580 "filters" in globalFilter581 ) {582 const filterObj = globalFilter as {583 filters: ExtendedColumnFilter<Product>[]584 }585 return filterObj.filters || []586 }587 return columnFilters588 .map(cf => cf.value)589 .filter(590 (v): v is ExtendedColumnFilter<Product> =>591 v !== null && typeof v === "object" && "id" in v,592 )593 }, [globalFilter, columnFilters])594
595 // Handler for filter menu596 const handleFiltersChange = useCallback(597 (filters: ExtendedColumnFilter<Product>[] | null) => {598 if (!filters || filters.length === 0) {599 setColumnFilters([])600 setGlobalFilter("")601 setPagination(prev => ({ ...prev, pageIndex: 0 }))602 } else {603 const hasOrFilters = filters.some(604 (filter, index) => index > 0 && filter.joinOperator === "or",605 )606 if (hasOrFilters) {607 setColumnFilters([])608 setGlobalFilter({609 filters,610 joinOperator: "mixed",611 })612 setPagination(prev => ({ ...prev, pageIndex: 0 }))613 } else {614 setGlobalFilter("")615 setColumnFilters(616 filters.map(filter => ({617 id: filter.id,618 value: filter,619 })),620 )621 setPagination(prev => ({ ...prev, pageIndex: 0 }))622 }623 }624 },625 [],626 )627
628 // Helper to display global filter state629 const getGlobalFilterDisplay = () => {630 if (typeof globalFilter === "string") {631 return globalFilter || "None"632 }633 if (634 typeof globalFilter === "object" &&635 globalFilter &&636 "filters" in globalFilter637 ) {638 const filterObj = globalFilter as {639 filters: unknown[]640 joinOperator: string641 }642 return `OR Filter (${filterObj.filters?.length || 0} conditions)`643 }644 return "None"645 }646
647 // Extract actual filter data for display648 const displayFilters = useMemo(() => {649 if (650 typeof globalFilter === "object" &&651 globalFilter &&652 "filters" in globalFilter653 ) {654 const filterObj = globalFilter as {655 filters: unknown[]656 joinOperator: string657 }658 return filterObj.filters || []659 }660 return columnFilters661 }, [columnFilters, globalFilter])662
663 // Enhanced filter statistics664 const filterStats = useMemo(() => {665 if (666 typeof globalFilter === "object" &&667 globalFilter &&668 "filters" in globalFilter669 ) {670 const filterObj = globalFilter as {671 filters: Array<{672 joinOperator?: string673 value?: unknown674 }>675 joinOperator: string676 }677 const filters = filterObj.filters || []678
679 const hasAndFilters = filters.some(680 (filter, index) =>681 index === 0 || filter.joinOperator === JOIN_OPERATORS.AND,682 )683 const hasOrFilters = filters.some(684 (filter, index) =>685 index > 0 && filter.joinOperator === JOIN_OPERATORS.OR,686 )687
688 return {689 totalFilters: filters.length,690 hasAndFilters,691 hasOrFilters,692 effectiveJoinOperator: hasOrFilters693 ? JOIN_OPERATORS.MIXED694 : JOIN_OPERATORS.AND,695 activeFilters: filters.filter(f => f.value && f.value !== "").length,696 }697 }698
699 const hasAndFilters = columnFilters.length > 0700 const hasOrFilters = columnFilters.some(701 filter =>702 typeof filter.value === "object" &&703 filter.value &&704 "joinOperator" in filter.value &&705 filter.value.joinOperator === "or",706 )707
708 return {709 totalFilters: columnFilters.length,710 hasAndFilters,711 hasOrFilters,712 effectiveJoinOperator: hasOrFilters713 ? JOIN_OPERATORS.MIXED714 : JOIN_OPERATORS.AND,715 activeFilters: columnFilters.filter(f => f.value && f.value !== "")716 .length,717 }718 }, [columnFilters, globalFilter])719
720 // Get current filter mode721 const getFilterMode = () => {722 if (723 typeof globalFilter === "object" &&724 globalFilter &&725 "filters" in globalFilter726 ) {727 const filterObj = globalFilter as {728 filters: unknown[]729 joinOperator: string730 }731 if (filterObj.joinOperator === "mixed") {732 return "MIXED"733 }734 return filterObj.joinOperator.toUpperCase()735 }736
737 const hasOrOperators = columnFilters.some(738 filter =>739 typeof filter.value === "object" &&740 filter.value &&741 "joinOperator" in filter.value &&742 filter.value.joinOperator === "or",743 )744
745 return hasOrOperators ? "MIXED" : "AND"746 }747
748 // Define columns with all features749 const columns: DataTableColumnDef<Product>[] = useMemo(750 () => [751 {752 id: SYSTEM_COLUMN_IDS.SELECT,753 size: 40, // Compact width for checkbox column754 header: ({ table }) => (755 <Checkbox756 checked={757 table.getIsAllPageRowsSelected() ||758 (table.getIsSomePageRowsSelected() && "indeterminate")759 }760 onCheckedChange={value => table.toggleAllPageRowsSelected(!!value)}761 aria-label="Select all"762 />763 ),764 cell: ({ row }) => (765 <Checkbox766 checked={row.getIsSelected()}767 onCheckedChange={value => row.toggleSelected(!!value)}768 aria-label="Select row"769 />770 ),771 enableSorting: false,772 enableHiding: false,773 },774 {775 id: SYSTEM_COLUMN_IDS.EXPAND,776 header: () => null,777 cell: ({ row }) => {778 if (!row.getCanExpand()) return null779 return (780 <Button781 variant="ghost"782 size="sm"783 className="h-6 w-6 p-0"784 onClick={row.getToggleExpandedHandler()}785 >786 {row.getIsExpanded() ? (787 <ChevronDown className="h-4 w-4" />788 ) : (789 <ChevronRight className="h-4 w-4" />790 )}791 </Button>792 )793 },794 size: 50,795 enableSorting: false,796 enableHiding: false,797 meta: {798 expandedContent: (product: Product) => (799 <ExpandedRowContent product={product} />800 ),801 },802 },803 {804 accessorKey: "name",805 header: () => (806 <DataTableColumnHeader className="justify-start">807 <DataTableColumnTitle>Product Name</DataTableColumnTitle>808 <DataTableColumnActions>809 <DataTableColumnSortOptions withSeparator={false} />810 <DataTableColumnPinOptions />811 <DataTableColumnHideOptions />812 </DataTableColumnActions>813 </DataTableColumnHeader>814 ),815 meta: {816 label: "Product Name",817 variant: FILTER_VARIANTS.TEXT,818 },819 enableColumnFilter: true,820 cell: ({ row }) => (821 <div822 className="cursor-pointer font-medium hover:underline"823 onClick={() => {824 setSelectedProductId(row.original.id)825 }}826 >827 {row.getValue("name")}828 </div>829 ),830 },831 {832 accessorKey: "category",833 header: () => (834 <DataTableColumnHeader>835 <DataTableColumnTitle />836 {/* Composable Actions: Multi-select filter example */}837 <DataTableColumnActions label="Category Options">838 <DataTableColumnSortOptions839 variant={FILTER_VARIANTS.TEXT}840 withSeparator={false}841 />842 <DataTableColumnFacetedFilterOptions843 options={categoryOptions}844 multiple845 />846 <DataTableColumnPinOptions />847 <DataTableColumnHideOptions />848 </DataTableColumnActions>849 </DataTableColumnHeader>850 ),851 meta: {852 label: "Category",853 variant: FILTER_VARIANTS.SELECT,854 options: categoryOptions,855 },856 cell: ({ row }) => {857 const category = row.getValue("category") as string858 const option = categoryOptions.find(opt => opt.value === category)859 return <span>{option?.label || category}</span>860 },861 enableColumnFilter: true,862 },863 {864 accessorKey: "brand",865 header: () => (866 <DataTableColumnHeader>867 <DataTableColumnTitle />868 {/* Composable Actions: Single-select filter example */}869 <DataTableColumnActions label="Brand Options">870 <DataTableColumnSortOptions871 variant={FILTER_VARIANTS.TEXT}872 withSeparator={false}873 />874 <DataTableColumnFacetedFilterOptions875 options={brandOptions}876 multiple={false}877 />878 <DataTableColumnPinOptions />879 <DataTableColumnHideOptions />880 </DataTableColumnActions>881 </DataTableColumnHeader>882 ),883 meta: {884 label: "Brand",885 variant: FILTER_VARIANTS.SELECT,886 options: brandOptions,887 },888 enableColumnFilter: true,889 },890 {891 accessorKey: "price",892 header: () => (893 <DataTableColumnHeader>894 <DataTableColumnTitle />895 <DataTableColumnActions>896 <DataTableColumnSortOptions withSeparator={false} />897 <DataTableColumnSliderFilterOptions />898 <DataTableColumnPinOptions />899 <DataTableColumnHideOptions />900 </DataTableColumnActions>901 </DataTableColumnHeader>902 ),903 meta: {904 label: "Price",905 unit: "$",906 variant: FILTER_VARIANTS.RANGE,907 },908 cell: ({ row }) => {909 const price = parseFloat(row.getValue("price"))910 return <div className="font-medium">${price.toFixed(2)}</div>911 },912 enableColumnFilter: true,913 },914 {915 accessorKey: "stock",916 header: () => (917 <DataTableColumnHeader>918 <DataTableColumnTitle />919 {/* All actions composed in single dropdown */}920 <DataTableColumnActions>921 <DataTableColumnSortOptions922 variant={FILTER_VARIANTS.NUMBER}923 withSeparator={false}924 />925 <DataTableColumnPinOptions />926 <DataTableColumnHideOptions />927 </DataTableColumnActions>928 </DataTableColumnHeader>929 ),930 meta: {931 label: "Stock",932 variant: FILTER_VARIANTS.NUMBER,933 },934 cell: ({ row }) => {935 const stock = Number(row.getValue("stock"))936 return (937 <div className={stock < 10 ? "font-medium text-red-600" : ""}>938 {stock}939 </div>940 )941 },942 enableColumnFilter: true,943 },944 {945 accessorKey: "rating",946 header: () => (947 <DataTableColumnHeader>948 <DataTableColumnTitle />949 <DataTableColumnActions>950 <DataTableColumnSortOptions951 variant={FILTER_VARIANTS.NUMBER}952 withSeparator={false}953 />954 <DataTableColumnPinOptions />955 <DataTableColumnHideOptions />956 </DataTableColumnActions>957 </DataTableColumnHeader>958 ),959 meta: {960 label: "Rating",961 variant: FILTER_VARIANTS.NUMBER,962 },963 cell: ({ row }) => {964 const rating = Number(row.getValue("rating"))965 return (966 <div className="flex items-center gap-1">967 <span>{rating}</span>968 <span className="text-yellow-500">★</span>969 </div>970 )971 },972 enableColumnFilter: true,973 },974 {975 accessorKey: "inStock",976 header: () => (977 <DataTableColumnHeader>978 <DataTableColumnTitle />979 <DataTableColumnActions>980 <DataTableColumnSortOptions withSeparator={false} />981 <DataTableColumnPinOptions />982 <DataTableColumnHideOptions />983 </DataTableColumnActions>984 </DataTableColumnHeader>985 ),986 meta: {987 label: "In Stock",988 variant: FILTER_VARIANTS.BOOLEAN,989 },990 cell: ({ row }) => {991 const inStock = Boolean(row.getValue("inStock"))992 return (993 <Badge variant={inStock ? "default" : "secondary"}>994 {inStock ? "Yes" : "No"}995 </Badge>996 )997 },998 enableColumnFilter: true,999 },1000 {1001 accessorKey: "releaseDate",1002 header: () => (1003 <DataTableColumnHeader>1004 <DataTableColumnTitle />1005 <DataTableColumnActions>1006 <DataTableColumnSortOptions withSeparator={false} />1007 <DataTableColumnDateFilterOptions />1008 <DataTableColumnPinOptions />1009 <DataTableColumnHideOptions />1010 </DataTableColumnActions>1011 </DataTableColumnHeader>1012 ),1013 meta: {1014 label: "Release Date",1015 variant: FILTER_VARIANTS.DATE,1016 },1017 cell: ({ row }) => {1018 const date = row.getValue("releaseDate") as Date1019 return <span>{date.toLocaleDateString()}</span>1020 },1021 enableColumnFilter: true,1022 },1023 {1024 id: "actions",1025 header: () => <div className="text-right">Actions</div>,1026 cell: ({ row }) => {1027 const product = row.original1028 return (1029 <div className="flex justify-end">1030 <DropdownMenu>1031 <DropdownMenuTrigger asChild>1032 <Button variant="ghost" className="h-8 w-8 p-0">1033 <MoreHorizontal className="h-4 w-4" />1034 </Button>1035 </DropdownMenuTrigger>1036 <DropdownMenuContent align="end">1037 <DropdownMenuItem1038 onClick={() => {1039 setSelectedProductId(product.id)1040 }}1041 >1042 View Details1043 </DropdownMenuItem>1044 <DropdownMenuItem1045 onClick={() => console.log("Edit", product.id)}1046 >1047 Edit1048 </DropdownMenuItem>1049 <DropdownMenuItem1050 onClick={() => console.log("Delete", product.id)}1051 className="text-red-600"1052 >1053 Delete1054 </DropdownMenuItem>1055 </DropdownMenuContent>1056 </DropdownMenu>1057 </div>1058 )1059 },1060 enableSorting: false,1061 enableHiding: false,1062 },1063 ],1064 [],1065 )1066
1067 return (1068 <div className="w-full space-y-4">1069 <DataTableRoot1070 data={data}1071 columns={columns}1072 config={{1073 enablePagination: true,1074 enableSorting: true,1075 enableMultiSort: true,1076 enableFilters: true,1077 enableRowSelection: true,1078 enableExpanding: true,1079 }}1080 getRowCanExpand={() => true}1081 getSubRows={() => undefined}1082 state={{1083 globalFilter,1084 sorting,1085 columnFilters,1086 columnVisibility,1087 rowSelection,1088 expanded,1089 columnPinning,1090 pagination,1091 }}1092 onGlobalFilterChange={value => {1093 setGlobalFilter(value)1094 setPagination(prev => ({ ...prev, pageIndex: 0 }))1095 }}1096 onSortingChange={setSorting}1097 onColumnFiltersChange={setColumnFilters}1098 onColumnVisibilityChange={setColumnVisibility}1099 onRowSelectionChange={setRowSelection}1100 onExpandedChange={setExpanded}1101 onColumnPinningChange={setColumnPinning}1102 onPaginationChange={setPagination}1103 >1104 <FilterToolbar1105 filters={currentFilters}1106 onFiltersChange={handleFiltersChange}1107 />1108 <BulkActions />1109
1110 {/* Sidebar Layout */}1111 <div className="flex min-h-150 gap-4">1112 {/* Main Table Area */}1113 <DataTable className="flex-1" height="100%">1114 <DataTableHeader />1115 <DataTableBody1116 onRowClick={(product: Product) => {1117 console.log("Row clicked:", product.id)1118 setSelectedProductId(product.id)1119 }}1120 >1121 <DataTableEmptyBody>1122 <DataTableEmptyMessage>1123 <DataTableEmptyIcon>1124 <UserSearch className="size-12" />1125 </DataTableEmptyIcon>1126 <DataTableEmptyTitle>No products found</DataTableEmptyTitle>1127 <DataTableEmptyDescription>1128 Get started by adding your first product to the inventory.1129 </DataTableEmptyDescription>1130 </DataTableEmptyMessage>1131 <DataTableEmptyFilteredMessage>1132 <DataTableEmptyIcon>1133 <SearchX className="size-12" />1134 </DataTableEmptyIcon>1135 <DataTableEmptyTitle>No matches found</DataTableEmptyTitle>1136 <DataTableEmptyDescription>1137 Try adjusting your filters or search to find what1138 you're looking for.1139 </DataTableEmptyDescription>1140 </DataTableEmptyFilteredMessage>1141 <DataTableEmptyActions>1142 <Button onClick={() => alert("Add product clicked")}>1143 Add Product1144 </Button>1145 </DataTableEmptyActions>1146 </DataTableEmptyBody>1147 </DataTableBody>1148 </DataTable>1149
1150 {/* Right Sidebar - Product Details */}1151 {selectedProduct && (1152 <DataTableAside1153 side="right"1154 open={!!selectedProduct}1155 onOpenChange={open => {1156 if (!open) setSelectedProductId(null)1157 }}1158 >1159 <DataTableAsideContent width="w-78">1160 <DataTableAsideHeader>1161 <DataTableAsideTitle>Product Details</DataTableAsideTitle>1162 <DataTableAsideDescription>1163 View detailed information1164 </DataTableAsideDescription>1165 <DataTableAsideClose />1166 </DataTableAsideHeader>1167 <ProductDetails product={selectedProduct} />1168 </DataTableAsideContent>1169 </DataTableAside>1170 )}1171 </div>1172 <DataTablePagination />1173 </DataTableRoot>1174
1175 {/* State Display */}1176 <Card>1177 <CardHeader>1178 <CardTitle>Current Table State</CardTitle>1179 <CardDescription>1180 Live view of all table state for demonstration1181 </CardDescription>1182 <CardAction>1183 <Button variant="outline" size="sm" onClick={resetAllState}>1184 Reset All State1185 </Button>1186 </CardAction>1187 </CardHeader>1188 <CardContent className="space-y-4">1189 <div className="grid gap-2 text-xs text-muted-foreground">1190 <div className="flex justify-between">1191 <span className="font-medium">Search Query:</span>1192 <span className="text-foreground">1193 {getGlobalFilterDisplay()}1194 </span>1195 </div>1196
1197 <div className="flex justify-between">1198 <span className="font-medium">Total Items:</span>1199 <span className="text-foreground">{data.length}</span>1200 </div>1201
1202 <div className="flex justify-between">1203 <span className="font-medium">Selected Rows:</span>1204 <span className="text-foreground">1205 {1206 Object.keys(rowSelection).filter(key => rowSelection[key])1207 .length1208 }1209 </span>1210 </div>1211
1212 <div className="flex justify-between">1213 <span className="font-medium">Expanded Rows:</span>1214 <span className="text-foreground">1215 {typeof expanded === "object" && expanded !== null1216 ? Object.keys(expanded).filter(1217 key => (expanded as Record<string, boolean>)[key],1218 ).length1219 : 0}1220 </span>1221 </div>1222
1223 <div className="flex justify-between">1224 <span className="font-medium">Active Filters:</span>1225 <span className="text-foreground">{columnFilters.length}</span>1226 </div>1227
1228 <div className="flex justify-between">1229 <span className="font-medium">Enhanced Filters:</span>1230 <span className="text-foreground">1231 {filterStats.totalFilters}1232 </span>1233 </div>1234
1235 <div className="flex justify-between">1236 <span className="font-medium">Active Enhanced:</span>1237 <span className="text-foreground">1238 {filterStats.activeFilters}1239 </span>1240 </div>1241
1242 <div className="flex justify-between">1243 <span className="font-medium">Join Logic:</span>1244 <span className="text-foreground">1245 {filterStats.effectiveJoinOperator}1246 </span>1247 </div>1248
1249 <div className="flex justify-between">1250 <span className="font-medium">Sorting:</span>1251 <span className="text-foreground">1252 {sorting.length > 01253 ? sorting1254 .map(s => `${s.id} ${s.desc ? "desc" : "asc"}`)1255 .join(", ")1256 : "None"}1257 </span>1258 </div>1259
1260 <div className="flex justify-between">1261 <span className="font-medium">Page:</span>1262 <span className="text-foreground">1263 {pagination.pageIndex + 1} (Size: {pagination.pageSize})1264 </span>1265 </div>1266
1267 <div className="flex justify-between">1268 <span className="font-medium">Hidden Columns:</span>1269 <span className="text-foreground">1270 {1271 Object.values(columnVisibility).filter(v => v === false)1272 .length1273 }1274 </span>1275 </div>1276
1277 <div className="flex justify-between">1278 <span className="font-medium">Pinned Columns:</span>1279 <span className="text-foreground">1280 {columnPinning.left?.length || 0} Left,{" "}1281 {columnPinning.right?.length || 0} Right1282 </span>1283 </div>1284 </div>1285
1286 {/* Detailed state (collapsible) */}1287 <details className="border-t pt-4">1288 <summary className="cursor-pointer text-xs font-medium hover:text-foreground">1289 View Full State Object1290 </summary>1291 <div className="mt-4 space-y-3 text-xs">1292 <div>1293 <strong>Enhanced Filters:</strong>1294 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1295 {displayFilters.length > 01296 ? JSON.stringify(displayFilters, null, 2)1297 : "No enhanced filters"}1298 </pre>1299 </div>1300 <div>1301 <strong>Column Pinning:</strong>1302 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1303 {JSON.stringify(columnPinning, null, 2)}1304 </pre>1305 </div>1306 <div>1307 <strong>Filter Stats:</strong>1308 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1309 {JSON.stringify(filterStats, null, 2)}1310 </pre>1311 </div>1312 <div>1313 <strong>Filter Mode:</strong> {getFilterMode()}1314 <div className="mt-1 text-muted-foreground">1315 {getFilterMode() === "AND"1316 ? "All conditions must match (stored in columnFilters)"1317 : getFilterMode() === "OR"1318 ? "Any condition can match (stored in globalFilter)"1319 : "Mixed logic - individual AND/OR operators per filter"}1320 </div>1321 </div>1322 <div>1323 <strong>Sorting:</strong>1324 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1325 {JSON.stringify(sorting, null, 2)}1326 </pre>1327 </div>1328 <div>1329 <strong>Column Filters State (AND logic):</strong>1330 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1331 {JSON.stringify(columnFilters, null, 2)}1332 </pre>1333 </div>1334 <div>1335 <strong>Global Filter State (OR logic):</strong>1336 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1337 {JSON.stringify(globalFilter, null, 2)}1338 </pre>1339 </div>1340 <div>1341 <strong>Column Visibility:</strong>1342 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1343 {JSON.stringify(columnVisibility, null, 2)}1344 </pre>1345 </div>1346 <div>1347 <strong>Row Selection:</strong>1348 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1349 {JSON.stringify(rowSelection, null, 2)}1350 </pre>1351 </div>1352 <div>1353 <strong>Expanded Rows:</strong>1354 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1355 {JSON.stringify(expanded, null, 2)}1356 </pre>1357 </div>1358 </div>1359 </details>1360 </CardContent>1361 </Card>1362 </div>1363 )1364}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 components (install only the registry pieces you need)
- 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!