Niko Table
Build production-ready data tables with sorting, filtering, pagination, virtualization, and more.
Nobody’s table, everyone’s solution.
Just like Shadcn, Niko Table is not a component library. Instead, it’s a comprehensive DataTable component that you copy into your project and customize to your needs. Built with TanStack Table and Shadcn UI, it provides enterprise-grade features while remaining fully under your control.
npx shadcn@latest add https://niko-table.com/r/data-table.jsonLive Demo
Section titled “Live Demo”See ALL features in action with this comprehensive example:
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 multiple515 />516 <DataTableFacetedFilter517 accessorKey="brand"518 title="Brand"519 options={brandOptions}520 limitToFilteredRows521 multiple522 />523 <DataTableSliderFilter accessorKey="price" />524 <DataTableSortMenu />525 <DataTableFilterMenu526 filters={filters}527 onFiltersChange={onFiltersChange}528 />529 <DataTableClearFilter />530 </DataTableToolbarSection>531 </DataTableToolbarSection>532 )533}534
535export default function AllFeaturesTableExample() {536 // Controlled state management537 const [data] = useState<Product[]>(initialData)538 const [globalFilter, setGlobalFilter] = useState<string | object>("")539 const [sorting, setSorting] = useState<SortingState>([])540 const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])541 const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})542 const [rowSelection, setRowSelection] = useState<RowSelectionState>({})543 const [expanded, setExpanded] = useState<ExpandedState>({})544 const [pagination, setPagination] = useState<PaginationState>({545 pageIndex: 0,546 pageSize: 10,547 })548 const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({549 left: [],550 right: [],551 })552
553 // Sidebar state554 const [selectedProductId, setSelectedProductId] = useState<string | null>(555 null,556 )557
558 const selectedProduct = selectedProductId559 ? data.find(product => product.id === selectedProductId)560 : null561
562 const resetAllState = useCallback(() => {563 setGlobalFilter("")564 setSorting([])565 setColumnFilters([])566 setColumnVisibility({})567 setRowSelection({})568 setExpanded({})569 setColumnPinning({ left: [], right: [] })570 setPagination({ pageIndex: 0, pageSize: 10 })571 setSelectedProductId(null)572 }, [])573
574 // Extract filters for display575 const currentFilters = useMemo(() => {576 if (577 typeof globalFilter === "object" &&578 globalFilter &&579 "filters" in globalFilter580 ) {581 const filterObj = globalFilter as {582 filters: ExtendedColumnFilter<Product>[]583 }584 return filterObj.filters || []585 }586 return columnFilters587 .map(cf => cf.value)588 .filter(589 (v): v is ExtendedColumnFilter<Product> =>590 v !== null && typeof v === "object" && "id" in v,591 )592 }, [globalFilter, columnFilters])593
594 // Handler for filter menu595 const handleFiltersChange = useCallback(596 (filters: ExtendedColumnFilter<Product>[] | null) => {597 if (!filters || filters.length === 0) {598 setColumnFilters([])599 setGlobalFilter("")600 setPagination(prev => ({ ...prev, pageIndex: 0 }))601 } else {602 const hasOrFilters = filters.some(603 (filter, index) => index > 0 && filter.joinOperator === "or",604 )605 if (hasOrFilters) {606 setColumnFilters([])607 setGlobalFilter({608 filters,609 joinOperator: "mixed",610 })611 setPagination(prev => ({ ...prev, pageIndex: 0 }))612 } else {613 setGlobalFilter("")614 setColumnFilters(615 filters.map(filter => ({616 id: filter.id,617 value: filter,618 })),619 )620 setPagination(prev => ({ ...prev, pageIndex: 0 }))621 }622 }623 },624 [],625 )626
627 // Helper to display global filter state628 const getGlobalFilterDisplay = () => {629 if (typeof globalFilter === "string") {630 return globalFilter || "None"631 }632 if (633 typeof globalFilter === "object" &&634 globalFilter &&635 "filters" in globalFilter636 ) {637 const filterObj = globalFilter as {638 filters: unknown[]639 joinOperator: string640 }641 return `OR Filter (${filterObj.filters?.length || 0} conditions)`642 }643 return "None"644 }645
646 // Extract actual filter data for display647 const displayFilters = useMemo(() => {648 if (649 typeof globalFilter === "object" &&650 globalFilter &&651 "filters" in globalFilter652 ) {653 const filterObj = globalFilter as {654 filters: unknown[]655 joinOperator: string656 }657 return filterObj.filters || []658 }659 return columnFilters660 }, [columnFilters, globalFilter])661
662 // Enhanced filter statistics663 const filterStats = useMemo(() => {664 if (665 typeof globalFilter === "object" &&666 globalFilter &&667 "filters" in globalFilter668 ) {669 const filterObj = globalFilter as {670 filters: Array<{671 joinOperator?: string672 value?: unknown673 }>674 joinOperator: string675 }676 const filters = filterObj.filters || []677
678 const hasAndFilters = filters.some(679 (filter, index) =>680 index === 0 || filter.joinOperator === JOIN_OPERATORS.AND,681 )682 const hasOrFilters = filters.some(683 (filter, index) =>684 index > 0 && filter.joinOperator === JOIN_OPERATORS.OR,685 )686
687 return {688 totalFilters: filters.length,689 hasAndFilters,690 hasOrFilters,691 effectiveJoinOperator: hasOrFilters692 ? JOIN_OPERATORS.MIXED693 : JOIN_OPERATORS.AND,694 activeFilters: filters.filter(f => f.value && f.value !== "").length,695 }696 }697
698 const hasAndFilters = columnFilters.length > 0699 const hasOrFilters = columnFilters.some(700 filter =>701 typeof filter.value === "object" &&702 filter.value &&703 "joinOperator" in filter.value &&704 filter.value.joinOperator === "or",705 )706
707 return {708 totalFilters: columnFilters.length,709 hasAndFilters,710 hasOrFilters,711 effectiveJoinOperator: hasOrFilters712 ? JOIN_OPERATORS.MIXED713 : JOIN_OPERATORS.AND,714 activeFilters: columnFilters.filter(f => f.value && f.value !== "")715 .length,716 }717 }, [columnFilters, globalFilter])718
719 // Get current filter mode720 const getFilterMode = () => {721 if (722 typeof globalFilter === "object" &&723 globalFilter &&724 "filters" in globalFilter725 ) {726 const filterObj = globalFilter as {727 filters: unknown[]728 joinOperator: string729 }730 if (filterObj.joinOperator === "mixed") {731 return "MIXED"732 }733 return filterObj.joinOperator.toUpperCase()734 }735
736 const hasOrOperators = columnFilters.some(737 filter =>738 typeof filter.value === "object" &&739 filter.value &&740 "joinOperator" in filter.value &&741 filter.value.joinOperator === "or",742 )743
744 return hasOrOperators ? "MIXED" : "AND"745 }746
747 // Define columns with all features748 const columns: DataTableColumnDef<Product>[] = useMemo(749 () => [750 {751 id: SYSTEM_COLUMN_IDS.SELECT,752 size: 40, // Compact width for checkbox column753 header: ({ table }) => (754 <Checkbox755 checked={756 table.getIsAllPageRowsSelected() ||757 (table.getIsSomePageRowsSelected() && "indeterminate")758 }759 onCheckedChange={value => table.toggleAllPageRowsSelected(!!value)}760 aria-label="Select all"761 />762 ),763 cell: ({ row }) => (764 <Checkbox765 checked={row.getIsSelected()}766 onCheckedChange={value => row.toggleSelected(!!value)}767 aria-label="Select row"768 />769 ),770 enableSorting: false,771 enableHiding: false,772 },773 {774 id: SYSTEM_COLUMN_IDS.EXPAND,775 header: () => null,776 cell: ({ row }) => {777 if (!row.getCanExpand()) return null778 return (779 <Button780 variant="ghost"781 size="sm"782 className="h-6 w-6 p-0"783 onClick={row.getToggleExpandedHandler()}784 >785 {row.getIsExpanded() ? (786 <ChevronDown className="h-4 w-4" />787 ) : (788 <ChevronRight className="h-4 w-4" />789 )}790 </Button>791 )792 },793 size: 50,794 enableSorting: false,795 enableHiding: false,796 meta: {797 expandedContent: (product: Product) => (798 <ExpandedRowContent product={product} />799 ),800 },801 },802 {803 accessorKey: "name",804 header: () => (805 <DataTableColumnHeader className="justify-start">806 <DataTableColumnTitle>Product Name</DataTableColumnTitle>807 <DataTableColumnActions>808 <DataTableColumnSortOptions withSeparator={false} />809 <DataTableColumnPinOptions />810 <DataTableColumnHideOptions />811 </DataTableColumnActions>812 </DataTableColumnHeader>813 ),814 meta: {815 label: "Product Name",816 variant: FILTER_VARIANTS.TEXT,817 },818 enableColumnFilter: true,819 cell: ({ row }) => (820 <div821 className="cursor-pointer font-medium hover:underline"822 onClick={() => {823 setSelectedProductId(row.original.id)824 }}825 >826 {row.getValue("name")}827 </div>828 ),829 },830 {831 accessorKey: "category",832 header: () => (833 <DataTableColumnHeader>834 <DataTableColumnTitle />835 {/* Composable Actions: Multi-select filter example */}836 <DataTableColumnActions label="Category Options">837 <DataTableColumnSortOptions838 variant={FILTER_VARIANTS.TEXT}839 withSeparator={false}840 />841 <DataTableColumnFacetedFilterOptions842 options={categoryOptions}843 multiple844 />845 <DataTableColumnPinOptions />846 <DataTableColumnHideOptions />847 </DataTableColumnActions>848 </DataTableColumnHeader>849 ),850 meta: {851 label: "Category",852 variant: FILTER_VARIANTS.SELECT,853 options: categoryOptions,854 },855 cell: ({ row }) => {856 const category = row.getValue("category") as string857 const option = categoryOptions.find(opt => opt.value === category)858 return <span>{option?.label || category}</span>859 },860 enableColumnFilter: true,861 },862 {863 accessorKey: "brand",864 header: () => (865 <DataTableColumnHeader>866 <DataTableColumnTitle />867 {/* Composable Actions: Single-select filter example */}868 <DataTableColumnActions label="Brand Options">869 <DataTableColumnSortOptions870 variant={FILTER_VARIANTS.TEXT}871 withSeparator={false}872 />873 <DataTableColumnFacetedFilterOptions874 options={brandOptions}875 multiple={false}876 />877 <DataTableColumnPinOptions />878 <DataTableColumnHideOptions />879 </DataTableColumnActions>880 </DataTableColumnHeader>881 ),882 meta: {883 label: "Brand",884 variant: FILTER_VARIANTS.SELECT,885 options: brandOptions,886 },887 enableColumnFilter: true,888 },889 {890 accessorKey: "price",891 header: () => (892 <DataTableColumnHeader>893 <DataTableColumnTitle />894 <DataTableColumnActions>895 <DataTableColumnSortOptions withSeparator={false} />896 <DataTableColumnSliderFilterOptions />897 <DataTableColumnPinOptions />898 <DataTableColumnHideOptions />899 </DataTableColumnActions>900 </DataTableColumnHeader>901 ),902 meta: {903 label: "Price",904 unit: "$",905 variant: FILTER_VARIANTS.RANGE,906 },907 cell: ({ row }) => {908 const price = parseFloat(row.getValue("price"))909 return <div className="font-medium">${price.toFixed(2)}</div>910 },911 enableColumnFilter: true,912 },913 {914 accessorKey: "stock",915 header: () => (916 <DataTableColumnHeader>917 <DataTableColumnTitle />918 {/* All actions composed in single dropdown */}919 <DataTableColumnActions>920 <DataTableColumnSortOptions921 variant={FILTER_VARIANTS.NUMBER}922 withSeparator={false}923 />924 <DataTableColumnPinOptions />925 <DataTableColumnHideOptions />926 </DataTableColumnActions>927 </DataTableColumnHeader>928 ),929 meta: {930 label: "Stock",931 variant: FILTER_VARIANTS.NUMBER,932 },933 cell: ({ row }) => {934 const stock = Number(row.getValue("stock"))935 return (936 <div className={stock < 10 ? "font-medium text-red-600" : ""}>937 {stock}938 </div>939 )940 },941 enableColumnFilter: true,942 },943 {944 accessorKey: "rating",945 header: () => (946 <DataTableColumnHeader>947 <DataTableColumnTitle />948 <DataTableColumnActions>949 <DataTableColumnSortOptions950 variant={FILTER_VARIANTS.NUMBER}951 withSeparator={false}952 />953 <DataTableColumnPinOptions />954 <DataTableColumnHideOptions />955 </DataTableColumnActions>956 </DataTableColumnHeader>957 ),958 meta: {959 label: "Rating",960 variant: FILTER_VARIANTS.NUMBER,961 },962 cell: ({ row }) => {963 const rating = Number(row.getValue("rating"))964 return (965 <div className="flex items-center gap-1">966 <span>{rating}</span>967 <span className="text-yellow-500">★</span>968 </div>969 )970 },971 enableColumnFilter: true,972 },973 {974 accessorKey: "inStock",975 header: () => (976 <DataTableColumnHeader>977 <DataTableColumnTitle />978 <DataTableColumnActions>979 <DataTableColumnSortOptions withSeparator={false} />980 <DataTableColumnPinOptions />981 <DataTableColumnHideOptions />982 </DataTableColumnActions>983 </DataTableColumnHeader>984 ),985 meta: {986 label: "In Stock",987 variant: FILTER_VARIANTS.BOOLEAN,988 },989 cell: ({ row }) => {990 const inStock = Boolean(row.getValue("inStock"))991 return (992 <Badge variant={inStock ? "default" : "secondary"}>993 {inStock ? "Yes" : "No"}994 </Badge>995 )996 },997 enableColumnFilter: true,998 },999 {1000 accessorKey: "releaseDate",1001 header: () => (1002 <DataTableColumnHeader>1003 <DataTableColumnTitle />1004 <DataTableColumnActions>1005 <DataTableColumnSortOptions withSeparator={false} />1006 <DataTableColumnDateFilterOptions />1007 <DataTableColumnPinOptions />1008 <DataTableColumnHideOptions />1009 </DataTableColumnActions>1010 </DataTableColumnHeader>1011 ),1012 meta: {1013 label: "Release Date",1014 variant: FILTER_VARIANTS.DATE,1015 },1016 cell: ({ row }) => {1017 const date = row.getValue("releaseDate") as Date1018 return <span>{date.toLocaleDateString()}</span>1019 },1020 enableColumnFilter: true,1021 },1022 {1023 id: "actions",1024 header: () => <div className="text-right">Actions</div>,1025 cell: ({ row }) => {1026 const product = row.original1027 return (1028 <div className="flex justify-end">1029 <DropdownMenu>1030 <DropdownMenuTrigger asChild>1031 <Button variant="ghost" className="h-8 w-8 p-0">1032 <MoreHorizontal className="h-4 w-4" />1033 </Button>1034 </DropdownMenuTrigger>1035 <DropdownMenuContent align="end">1036 <DropdownMenuItem1037 onClick={() => {1038 setSelectedProductId(product.id)1039 }}1040 >1041 View Details1042 </DropdownMenuItem>1043 <DropdownMenuItem1044 onClick={() => console.log("Edit", product.id)}1045 >1046 Edit1047 </DropdownMenuItem>1048 <DropdownMenuItem1049 onClick={() => console.log("Delete", product.id)}1050 className="text-red-600"1051 >1052 Delete1053 </DropdownMenuItem>1054 </DropdownMenuContent>1055 </DropdownMenu>1056 </div>1057 )1058 },1059 enableSorting: false,1060 enableHiding: false,1061 },1062 ],1063 [],1064 )1065
1066 return (1067 <div className="w-full space-y-4">1068 <DataTableRoot1069 data={data}1070 columns={columns}1071 config={{1072 enablePagination: true,1073 enableSorting: true,1074 enableMultiSort: true,1075 enableFilters: true,1076 enableRowSelection: true,1077 enableExpanding: true,1078 }}1079 getRowCanExpand={() => true}1080 getSubRows={() => undefined}1081 state={{1082 globalFilter,1083 sorting,1084 columnFilters,1085 columnVisibility,1086 rowSelection,1087 expanded,1088 columnPinning,1089 pagination,1090 }}1091 onGlobalFilterChange={value => {1092 setGlobalFilter(value)1093 setPagination(prev => ({ ...prev, pageIndex: 0 }))1094 }}1095 onSortingChange={setSorting}1096 onColumnFiltersChange={setColumnFilters}1097 onColumnVisibilityChange={setColumnVisibility}1098 onRowSelectionChange={setRowSelection}1099 onExpandedChange={setExpanded}1100 onColumnPinningChange={setColumnPinning}1101 onPaginationChange={setPagination}1102 >1103 <FilterToolbar1104 filters={currentFilters}1105 onFiltersChange={handleFiltersChange}1106 />1107 <BulkActions />1108
1109 {/* Sidebar Layout */}1110 <div className="flex min-h-150 gap-4">1111 {/* Main Table Area */}1112 <DataTable className="flex-1" height="100%">1113 <DataTableHeader />1114 <DataTableBody1115 onRowClick={(product: Product) => {1116 console.log("Row clicked:", product.id)1117 setSelectedProductId(product.id)1118 }}1119 >1120 <DataTableEmptyBody>1121 <DataTableEmptyMessage>1122 <DataTableEmptyIcon>1123 <UserSearch className="size-12" />1124 </DataTableEmptyIcon>1125 <DataTableEmptyTitle>No products found</DataTableEmptyTitle>1126 <DataTableEmptyDescription>1127 Get started by adding your first product to the inventory.1128 </DataTableEmptyDescription>1129 </DataTableEmptyMessage>1130 <DataTableEmptyFilteredMessage>1131 <DataTableEmptyIcon>1132 <SearchX className="size-12" />1133 </DataTableEmptyIcon>1134 <DataTableEmptyTitle>No matches found</DataTableEmptyTitle>1135 <DataTableEmptyDescription>1136 Try adjusting your filters or search to find what1137 you're looking for.1138 </DataTableEmptyDescription>1139 </DataTableEmptyFilteredMessage>1140 <DataTableEmptyActions>1141 <Button onClick={() => alert("Add product clicked")}>1142 Add Product1143 </Button>1144 </DataTableEmptyActions>1145 </DataTableEmptyBody>1146 </DataTableBody>1147 </DataTable>1148
1149 {/* Right Sidebar - Product Details */}1150 {selectedProduct && (1151 <DataTableAside1152 side="right"1153 open={!!selectedProduct}1154 onOpenChange={open => {1155 if (!open) setSelectedProductId(null)1156 }}1157 >1158 <DataTableAsideContent width="w-78">1159 <DataTableAsideHeader>1160 <DataTableAsideTitle>Product Details</DataTableAsideTitle>1161 <DataTableAsideDescription>1162 View detailed information1163 </DataTableAsideDescription>1164 <DataTableAsideClose />1165 </DataTableAsideHeader>1166 <ProductDetails product={selectedProduct} />1167 </DataTableAsideContent>1168 </DataTableAside>1169 )}1170 </div>1171 <DataTablePagination />1172 </DataTableRoot>1173
1174 {/* State Display */}1175 <Card>1176 <CardHeader>1177 <CardTitle>Current Table State</CardTitle>1178 <CardDescription>1179 Live view of all table state for demonstration1180 </CardDescription>1181 <CardAction>1182 <Button variant="outline" size="sm" onClick={resetAllState}>1183 Reset All State1184 </Button>1185 </CardAction>1186 </CardHeader>1187 <CardContent className="space-y-4">1188 <div className="grid gap-2 text-xs text-muted-foreground">1189 <div className="flex justify-between">1190 <span className="font-medium">Search Query:</span>1191 <span className="text-foreground">1192 {getGlobalFilterDisplay()}1193 </span>1194 </div>1195
1196 <div className="flex justify-between">1197 <span className="font-medium">Total Items:</span>1198 <span className="text-foreground">{data.length}</span>1199 </div>1200
1201 <div className="flex justify-between">1202 <span className="font-medium">Selected Rows:</span>1203 <span className="text-foreground">1204 {1205 Object.keys(rowSelection).filter(key => rowSelection[key])1206 .length1207 }1208 </span>1209 </div>1210
1211 <div className="flex justify-between">1212 <span className="font-medium">Expanded Rows:</span>1213 <span className="text-foreground">1214 {typeof expanded === "object" && expanded !== null1215 ? Object.keys(expanded).filter(1216 key => (expanded as Record<string, boolean>)[key],1217 ).length1218 : 0}1219 </span>1220 </div>1221
1222 <div className="flex justify-between">1223 <span className="font-medium">Active Filters:</span>1224 <span className="text-foreground">{columnFilters.length}</span>1225 </div>1226
1227 <div className="flex justify-between">1228 <span className="font-medium">Enhanced Filters:</span>1229 <span className="text-foreground">1230 {filterStats.totalFilters}1231 </span>1232 </div>1233
1234 <div className="flex justify-between">1235 <span className="font-medium">Active Enhanced:</span>1236 <span className="text-foreground">1237 {filterStats.activeFilters}1238 </span>1239 </div>1240
1241 <div className="flex justify-between">1242 <span className="font-medium">Join Logic:</span>1243 <span className="text-foreground">1244 {filterStats.effectiveJoinOperator}1245 </span>1246 </div>1247
1248 <div className="flex justify-between">1249 <span className="font-medium">Sorting:</span>1250 <span className="text-foreground">1251 {sorting.length > 01252 ? sorting1253 .map(s => `${s.id} ${s.desc ? "desc" : "asc"}`)1254 .join(", ")1255 : "None"}1256 </span>1257 </div>1258
1259 <div className="flex justify-between">1260 <span className="font-medium">Page:</span>1261 <span className="text-foreground">1262 {pagination.pageIndex + 1} (Size: {pagination.pageSize})1263 </span>1264 </div>1265
1266 <div className="flex justify-between">1267 <span className="font-medium">Hidden Columns:</span>1268 <span className="text-foreground">1269 {1270 Object.values(columnVisibility).filter(v => v === false)1271 .length1272 }1273 </span>1274 </div>1275
1276 <div className="flex justify-between">1277 <span className="font-medium">Pinned Columns:</span>1278 <span className="text-foreground">1279 {columnPinning.left?.length || 0} Left,{" "}1280 {columnPinning.right?.length || 0} Right1281 </span>1282 </div>1283 </div>1284
1285 {/* Detailed state (collapsible) */}1286 <details className="border-t pt-4">1287 <summary className="cursor-pointer text-xs font-medium hover:text-foreground">1288 View Full State Object1289 </summary>1290 <div className="mt-4 space-y-3 text-xs">1291 <div>1292 <strong>Enhanced Filters:</strong>1293 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1294 {displayFilters.length > 01295 ? JSON.stringify(displayFilters, null, 2)1296 : "No enhanced filters"}1297 </pre>1298 </div>1299 <div>1300 <strong>Column Pinning:</strong>1301 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1302 {JSON.stringify(columnPinning, null, 2)}1303 </pre>1304 </div>1305 <div>1306 <strong>Filter Stats:</strong>1307 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1308 {JSON.stringify(filterStats, null, 2)}1309 </pre>1310 </div>1311 <div>1312 <strong>Filter Mode:</strong> {getFilterMode()}1313 <div className="mt-1 text-muted-foreground">1314 {getFilterMode() === "AND"1315 ? "All conditions must match (stored in columnFilters)"1316 : getFilterMode() === "OR"1317 ? "Any condition can match (stored in globalFilter)"1318 : "Mixed logic - individual AND/OR operators per filter"}1319 </div>1320 </div>1321 <div>1322 <strong>Sorting:</strong>1323 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1324 {JSON.stringify(sorting, null, 2)}1325 </pre>1326 </div>1327 <div>1328 <strong>Column Filters State (AND logic):</strong>1329 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1330 {JSON.stringify(columnFilters, null, 2)}1331 </pre>1332 </div>1333 <div>1334 <strong>Global Filter State (OR logic):</strong>1335 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1336 {JSON.stringify(globalFilter, null, 2)}1337 </pre>1338 </div>1339 <div>1340 <strong>Column Visibility:</strong>1341 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1342 {JSON.stringify(columnVisibility, null, 2)}1343 </pre>1344 </div>1345 <div>1346 <strong>Row Selection:</strong>1347 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1348 {JSON.stringify(rowSelection, null, 2)}1349 </pre>1350 </div>1351 <div>1352 <strong>Expanded Rows:</strong>1353 <pre className="mt-1 overflow-auto rounded bg-muted p-2">1354 {JSON.stringify(expanded, null, 2)}1355 </pre>1356 </div>1357 </div>1358 </details>1359 </CardContent>1360 </Card>1361 </div>1362 )1363}Product Name | Category | Brand | Price | Stock | Rating | In Stock | Release Date | Actions | ||
|---|---|---|---|---|---|---|---|---|---|---|
iPhone 15 Pro | Electronics | apple | $999.00 | 45 | 5★ | Yes | 5/10/2026 | |||
Galaxy S24 Ultra | Electronics | samsung | $1199.00 | 32 | 5★ | Yes | 5/5/2026 | |||
Air Jordan 1 | Sports | nike | $170.00 | 8 | 4★ | Yes | 4/20/2026 | |||
Ultraboost 23 | Sports | adidas | $190.00 | 15 | 4★ | Yes | 3/26/2026 | |||
PlayStation 5 | Electronics | sony | $499.00 | 0 | 5★ | No | 5/15/2025 | |||
OLED C3 TV | Electronics | lg | $1499.00 | 12 | 5★ | Yes | 2/14/2026 | |||
XPS 15 Laptop | Electronics | dell | $1899.00 | 20 | 4★ | Yes | 1/15/2026 | |||
Spectre x360 | Electronics | hp | $1599.00 | 18 | 4★ | Yes | 4/30/2026 | |||
MacBook Pro 16 | Electronics | apple | $2499.00 | 25 | 5★ | Yes | 4/15/2026 | |||
Galaxy Book3 | Electronics | samsung | $1399.00 | 14 | 4★ | Yes | 11/16/2025 |
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:
{}This demo showcases every feature:
- ✅ Row selection - Select individual or all rows with checkboxes
- ✅ Bulk actions - Export or delete selected rows
- ✅ Row expansion - Click expand icon to see additional details
- ✅ Sidebar panels - Left sidebar for quick filters, right sidebar for product details
- ✅ Multi-column sorting - Click column headers to sort
- ✅ Advanced filtering - Global search + column-specific filters with AND/OR logic
- ✅ Pagination - Navigate through pages with customizable page sizes
- ✅ Column visibility - Show/hide columns dynamically
- ✅ Data export - Export all or selected rows to CSV
- ✅ Controlled state - Full state management with React hooks
- ✅ Real-time updates - See state changes reflected immediately
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
- Advanced Table - All features combined
- Advanced Nuqs Table - URL state persistence
Key Features
Section titled “Key Features”- Type-safe - Full TypeScript support
- Accessible - WCAG 2.1 compliant with keyboard navigation
- Responsive - Mobile-friendly design
- Customizable - Full source code access
- Composable - Mix and match components
- Performance - Virtual scrolling for 10,000+ rows
- State Management - Context-based with hook flexibility
Built With
Section titled “Built With”- TanStack Table - Headless table utilities
- Shadcn UI - Beautiful UI components
- Tailwind CSS - Utility-first CSS
- Radix UI - Accessible primitives
- 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!