Advanced Filter Table
Rule-based filtering for complex datasets where users can define custom rules for data visibility.
"use client"
import { DataTableRoot, DataTable, DataTableHeader, DataTableBody, DataTableEmptyBody,} from "@/components/niko-table/core"import { DataTableToolbarSection, DataTablePagination, DataTableSearchFilter, DataTableViewMenu, DataTableSortMenu, DataTableFilterMenu, DataTableEmptyIcon, DataTableEmptyMessage, DataTableEmptyFilteredMessage, DataTableEmptyTitle, DataTableEmptyDescription, DataTableColumnTitle, DataTableColumnHeader, DataTableColumnSortMenu, DataTableColumnFacetedFilterMenu, DataTableColumnSliderFilterMenu, DataTableColumnDateFilterMenu,} from "@/components/niko-table/components"import { daysAgo, FILTER_VARIANTS } from "@/components/niko-table/lib"import type { DataTableColumnDef } from "@/components/niko-table/types"import { Badge } from "@/components/ui/badge"import { UserSearch, SearchX } from "lucide-react"
type Product = { id: string name: string category: string brand: string price: number stock: number rating: number inStock: boolean releaseDate: Date}
const categoryOptions = [ { label: "Electronics", value: "electronics" }, { label: "Clothing", value: "clothing" }, { label: "Home & Garden", value: "home-garden" }, { label: "Sports", value: "sports" }, { label: "Books", value: "books" },]
// Note: brand options are auto-generated from data via autoOptions; no static options needed
const columns: DataTableColumnDef<Product>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Product Name", variant: FILTER_VARIANTS.TEXT, }, enableColumnFilter: true, }, { accessorKey: "category", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> <DataTableColumnFacetedFilterMenu /> </DataTableColumnHeader> ), meta: { label: "Category", variant: FILTER_VARIANTS.SELECT, // Keep static options but augment with dynamic counts from data options: categoryOptions, mergeStrategy: "augment", dynamicCounts: true, showCounts: true, }, cell: ({ row }) => { const category = row.getValue("category") as string const option = categoryOptions.find(opt => opt.value === category) return <span>{option?.label || category}</span> }, enableColumnFilter: true, }, { accessorKey: "brand", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> <DataTableColumnFacetedFilterMenu /> </DataTableColumnHeader> ), meta: { label: "Brand", variant: FILTER_VARIANTS.SELECT, // Demonstrate auto-generation (no static options provided) autoOptions: true, dynamicCounts: true, showCounts: true, }, enableColumnFilter: true, }, { accessorKey: "price", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.NUMBER} /> <DataTableColumnSliderFilterMenu /> </DataTableColumnHeader> ), meta: { label: "Price", unit: "$", variant: FILTER_VARIANTS.NUMBER, }, cell: ({ row }) => { const price = parseFloat(row.getValue("price")) return <div className="font-medium">${price.toFixed(2)}</div> }, enableColumnFilter: true, }, { accessorKey: "stock", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.NUMBER} /> </DataTableColumnHeader> ), meta: { label: "Stock", variant: FILTER_VARIANTS.NUMBER, }, cell: ({ row }) => { const stock = Number(row.getValue("stock")) return ( <div className={stock < 10 ? "font-medium text-red-600" : ""}> {stock} </div> ) }, enableColumnFilter: true, }, { accessorKey: "rating", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.NUMBER} /> </DataTableColumnHeader> ), meta: { label: "Rating", variant: FILTER_VARIANTS.NUMBER, }, cell: ({ row }) => { const rating = Number(row.getValue("rating")) return ( <div className="flex items-center gap-1"> <span>{rating}</span> <span className="text-yellow-500">★</span> </div> ) }, enableColumnFilter: true, }, { accessorKey: "inStock", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> <DataTableColumnFacetedFilterMenu /> </DataTableColumnHeader> ), meta: { label: "In Stock", variant: FILTER_VARIANTS.BOOLEAN, }, cell: ({ row }) => { const inStock = Boolean(row.getValue("inStock")) return ( <Badge variant={inStock ? "default" : "secondary"}> {inStock ? "Yes" : "No"} </Badge> ) }, enableColumnFilter: true, }, { accessorKey: "releaseDate", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> <DataTableColumnDateFilterMenu /> </DataTableColumnHeader> ), meta: { label: "Release Date", variant: FILTER_VARIANTS.DATE, }, cell: ({ row }) => { const date = row.getValue("releaseDate") as Date return <span>{date.toLocaleDateString()}</span> }, enableColumnFilter: true, },]
const data: Product[] = [ { id: "1", name: "iPhone 15 Pro", category: "electronics", brand: "apple", price: 999, stock: 45, rating: 5, inStock: true, releaseDate: daysAgo(5), }, { id: "2", name: "Galaxy S24 Ultra", category: "electronics", brand: "samsung", price: 1199, stock: 32, rating: 5, inStock: true, releaseDate: daysAgo(10), }, { id: "3", name: "Air Jordan 1", category: "sports", brand: "nike", price: 170, stock: 8, rating: 4, inStock: true, releaseDate: daysAgo(25), }, { id: "4", name: "Ultraboost 23", category: "sports", brand: "adidas", price: 190, stock: 15, rating: 4, inStock: true, releaseDate: daysAgo(50), }, { id: "5", name: "PlayStation 5", category: "electronics", brand: "sony", price: 499, stock: 0, rating: 5, inStock: false, releaseDate: daysAgo(365), }, { id: "6", name: "OLED C3 TV", category: "electronics", brand: "lg", price: 1499, stock: 12, rating: 5, inStock: true, releaseDate: daysAgo(90), }, { id: "7", name: "XPS 15 Laptop", category: "electronics", brand: "dell", price: 1899, stock: 20, rating: 4, inStock: true, releaseDate: daysAgo(120), }, { id: "8", name: "Spectre x360", category: "electronics", brand: "hp", price: 1599, stock: 18, rating: 4, inStock: true, releaseDate: daysAgo(15), }, { id: "9", name: "MacBook Pro 16", category: "electronics", brand: "apple", price: 2499, stock: 25, rating: 5, inStock: true, releaseDate: daysAgo(30), }, { id: "10", name: "Galaxy Book3", category: "electronics", brand: "samsung", price: 1399, stock: 14, rating: 4, inStock: true, releaseDate: daysAgo(180), }, { id: "11", name: "Running Shorts", category: "clothing", brand: "nike", price: 45, stock: 120, rating: 3, inStock: true, releaseDate: daysAgo(60), }, { id: "12", name: "Training Jacket", category: "clothing", brand: "adidas", price: 85, stock: 65, rating: 4, inStock: true, releaseDate: daysAgo(45), }, { id: "13", name: "Garden Tools Set", category: "home-garden", brand: "hp", price: 120, stock: 30, rating: 4, inStock: true, releaseDate: daysAgo(75), }, { id: "14", name: "Programming Book", category: "books", brand: "dell", price: 60, stock: 50, rating: 5, inStock: true, releaseDate: daysAgo(200), }, { id: "15", name: "Wireless Mouse", category: "electronics", brand: "lg", price: 35, stock: 200, rating: 3, inStock: true, releaseDate: daysAgo(150), },]
function FilterToolbar() { return ( <DataTableToolbarSection> <DataTableToolbarSection className="px-0"> <DataTableSearchFilter placeholder="Search products..." /> <DataTableViewMenu /> </DataTableToolbarSection> <DataTableToolbarSection className="px-0"> <DataTableSortMenu className="ml-auto" /> <DataTableFilterMenu autoOptions dynamicCounts showCounts mergeStrategy="augment" /> </DataTableToolbarSection> </DataTableToolbarSection> )}
export default function AdvancedTableExample() { return ( <DataTableRoot data={data} columns={columns}> <FilterToolbar /> <DataTable> <DataTableHeader /> <DataTableBody> <DataTableEmptyBody> <DataTableEmptyMessage> <DataTableEmptyIcon> <UserSearch className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No products found</DataTableEmptyTitle> <DataTableEmptyDescription> Get started by adding your first product. </DataTableEmptyDescription> </DataTableEmptyMessage> <DataTableEmptyFilteredMessage> <DataTableEmptyIcon> <SearchX className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No matches found</DataTableEmptyTitle> <DataTableEmptyDescription> Try adjusting your filters or search to find what you're looking for. </DataTableEmptyDescription> </DataTableEmptyFilteredMessage> </DataTableEmptyBody> </DataTableBody> </DataTable> <DataTablePagination /> </DataTableRoot> )}Preview with Controlled State
View Full State Object
No enhanced filters
{
"left": [],
"right": []
}{
"totalFilters": 0,
"hasAndFilters": false,
"hasOrFilters": false,
"effectiveJoinOperator": "and",
"activeFilters": 0
}[]
[]
""
{}"use client"
import { useState, useCallback, useMemo } from "react"import type { PaginationState, SortingState, ColumnFiltersState, VisibilityState, ColumnPinningState, Updater,} from "@tanstack/react-table"import { DataTableRoot, DataTable, DataTableHeader, DataTableBody, DataTableEmptyBody,} from "@/components/niko-table/core"import { DataTableToolbarSection, DataTablePagination, DataTableSearchFilter, DataTableViewMenu, DataTableSortMenu, DataTableFilterMenu, DataTableEmptyIcon, DataTableEmptyMessage, DataTableEmptyFilteredMessage, DataTableEmptyTitle, DataTableEmptyDescription, DataTableColumnHeader, DataTableColumnTitle, DataTableColumnActions, DataTableColumnSortMenu, DataTableColumnFacetedFilterMenu, DataTableColumnSliderFilterMenu, DataTableColumnDateFilterMenu,} from "@/components/niko-table/components"import { daysAgo, JOIN_OPERATORS, processFiltersForLogic, FILTER_VARIANTS,} from "@/components/niko-table/lib"import type { DataTableColumnDef, ExtendedColumnFilter,} from "@/components/niko-table/types"import { Badge } from "@/components/ui/badge"import { Button } from "@/components/ui/button"import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card"import { UserSearch, SearchX } from "lucide-react"
type Product = { id: string name: string category: string brand: string price: number stock: number rating: number inStock: boolean releaseDate: Date}
const categoryOptions = [ { label: "Electronics", value: "electronics" }, { label: "Clothing", value: "clothing" }, { label: "Home & Garden", value: "home-garden" }, { label: "Sports", value: "sports" }, { label: "Books", value: "books" },]
const brandOptions = [ { label: "Apple", value: "apple" }, { label: "Samsung", value: "samsung" }, { label: "Nike", value: "nike" }, { label: "Adidas", value: "adidas" }, { label: "Sony", value: "sony" }, { label: "LG", value: "lg" }, { label: "Dell", value: "dell" }, { label: "HP", value: "hp" },]
const columns: DataTableColumnDef<Product>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader className="justify-start"> <DataTableColumnTitle>Product Name</DataTableColumnTitle> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Product Name", variant: FILTER_VARIANTS.TEXT, }, enableColumnFilter: true, cell: ({ row }) => ( <div className="font-medium">{row.getValue("name")}</div> ), }, { accessorKey: "category", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle>Category</DataTableColumnTitle> <DataTableColumnActions> <DataTableColumnFacetedFilterMenu options={categoryOptions} /> <DataTableColumnSortMenu /> </DataTableColumnActions> </DataTableColumnHeader> ), meta: { label: "Category", variant: FILTER_VARIANTS.SELECT, options: categoryOptions, }, cell: ({ row }) => { const category = row.getValue("category") as string const option = categoryOptions.find(opt => opt.value === category) return <span>{option?.label || category}</span> }, enableColumnFilter: true, }, { accessorKey: "brand", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle>Brand</DataTableColumnTitle> <DataTableColumnActions> <DataTableColumnFacetedFilterMenu options={brandOptions} /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> </DataTableColumnActions> </DataTableColumnHeader> ), meta: { label: "Brand", variant: FILTER_VARIANTS.SELECT, options: brandOptions, }, enableColumnFilter: true, }, { accessorKey: "price", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.NUMBER} /> <DataTableColumnSliderFilterMenu /> </DataTableColumnHeader> ), meta: { label: "Price", unit: "$", variant: FILTER_VARIANTS.NUMBER, }, cell: ({ row }) => { const price = parseFloat(row.getValue("price")) return <div className="font-medium">${price.toFixed(2)}</div> }, enableColumnFilter: true, }, { accessorKey: "stock", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.NUMBER} /> </DataTableColumnHeader> ), meta: { label: "Stock", variant: FILTER_VARIANTS.NUMBER, }, cell: ({ row }) => { const stock = Number(row.getValue("stock")) return ( <div className={stock < 10 ? "font-medium text-red-600" : ""}> {stock} </div> ) }, enableColumnFilter: true, }, { accessorKey: "rating", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.NUMBER} /> </DataTableColumnHeader> ), meta: { label: "Rating", variant: FILTER_VARIANTS.NUMBER, }, cell: ({ row }) => { const rating = Number(row.getValue("rating")) return ( <div className="flex items-center gap-1"> <span>{rating}</span> <span className="text-yellow-500">★</span> </div> ) }, enableColumnFilter: true, }, { accessorKey: "inStock", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> <DataTableColumnFacetedFilterMenu /> </DataTableColumnHeader> ), meta: { label: "In Stock", variant: FILTER_VARIANTS.BOOLEAN, }, cell: ({ row }) => { const inStock = Boolean(row.getValue("inStock")) return ( <Badge variant={inStock ? "default" : "secondary"}> {inStock ? "Yes" : "No"} </Badge> ) }, enableColumnFilter: true, }, { accessorKey: "releaseDate", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> <DataTableColumnDateFilterMenu /> </DataTableColumnHeader> ), meta: { label: "Release Date", variant: FILTER_VARIANTS.DATE, }, cell: ({ row }) => { const date = row.getValue("releaseDate") as Date return <span>{date.toLocaleDateString()}</span> }, enableColumnFilter: true, },]
const initialData: Product[] = [ { id: "1", name: "iPhone 15 Pro", category: "electronics", brand: "apple", price: 999, stock: 45, rating: 5, inStock: true, releaseDate: daysAgo(5), }, { id: "2", name: "Galaxy S24 Ultra", category: "electronics", brand: "samsung", price: 1199, stock: 32, rating: 5, inStock: true, releaseDate: daysAgo(10), }, { id: "3", name: "Air Jordan 1", category: "sports", brand: "nike", price: 170, stock: 8, rating: 4, inStock: true, releaseDate: daysAgo(25), }, { id: "4", name: "Ultraboost 23", category: "sports", brand: "adidas", price: 190, stock: 15, rating: 4, inStock: true, releaseDate: daysAgo(50), }, { id: "5", name: "PlayStation 5", category: "electronics", brand: "sony", price: 499, stock: 0, rating: 5, inStock: false, releaseDate: daysAgo(365), }, { id: "6", name: "OLED C3 TV", category: "electronics", brand: "lg", price: 1499, stock: 12, rating: 5, inStock: true, releaseDate: daysAgo(90), }, { id: "7", name: "XPS 15 Laptop", category: "electronics", brand: "dell", price: 1899, stock: 20, rating: 4, inStock: true, releaseDate: daysAgo(120), }, { id: "8", name: "Spectre x360", category: "electronics", brand: "hp", price: 1599, stock: 18, rating: 4, inStock: true, releaseDate: daysAgo(15), }, { id: "9", name: "MacBook Pro 16", category: "electronics", brand: "apple", price: 2499, stock: 25, rating: 5, inStock: true, releaseDate: daysAgo(30), }, { id: "10", name: "Galaxy Book3", category: "electronics", brand: "samsung", price: 1399, stock: 14, rating: 4, inStock: true, releaseDate: daysAgo(180), }, { id: "11", name: "Running Shorts", category: "clothing", brand: "nike", price: 45, stock: 120, rating: 3, inStock: true, releaseDate: daysAgo(60), }, { id: "12", name: "Training Jacket", category: "clothing", brand: "adidas", price: 85, stock: 65, rating: 4, inStock: true, releaseDate: daysAgo(45), }, { id: "13", name: "Garden Tools Set", category: "home-garden", brand: "hp", price: 120, stock: 30, rating: 4, inStock: true, releaseDate: daysAgo(75), }, { id: "14", name: "Programming Book", category: "books", brand: "dell", price: 60, stock: 50, rating: 5, inStock: true, releaseDate: daysAgo(200), }, { id: "15", name: "Wireless Mouse", category: "electronics", brand: "lg", price: 35, stock: 200, rating: 3, inStock: true, releaseDate: daysAgo(150), },]
function FilterToolbar({ filters, onFiltersChange,}: { filters: ExtendedColumnFilter<Product>[] onFiltersChange: (filters: ExtendedColumnFilter<Product>[] | null) => void}) { return ( <DataTableToolbarSection> <DataTableToolbarSection className="px-0"> <DataTableSearchFilter placeholder="Search products..." /> <DataTableViewMenu /> </DataTableToolbarSection> <DataTableToolbarSection className="px-0"> <DataTableSortMenu className="ml-auto" /> <DataTableFilterMenu filters={filters} onFiltersChange={onFiltersChange} /> </DataTableToolbarSection> </DataTableToolbarSection> )}
export default function AdvancedStateTableExample() { // State management with useState - tracking all table state const [data] = useState<Product[]>(initialData) const [globalFilter, setGlobalFilter] = useState<string | object>("") const [sorting, setSorting] = useState<SortingState>([]) const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]) const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({}) const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 10, }) const [columnPinning, setColumnPinning] = useState<ColumnPinningState>({ left: [], right: [], })
const resetAllState = useCallback(() => { setGlobalFilter("") setSorting([]) setColumnFilters([]) setColumnFilters([]) setColumnVisibility({}) setColumnPinning({ left: [], right: [] }) setPagination({ pageIndex: 0, pageSize: 10 }) }, [])
// Helper to display global filter state const getGlobalFilterDisplay = () => { if (typeof globalFilter === "string") { return globalFilter || "None" } if ( typeof globalFilter === "object" && globalFilter && "filters" in globalFilter ) { const filterObj = globalFilter as { filters: unknown[] joinOperator: string } return `OR Filter (${filterObj.filters?.length || 0} conditions)` } return "None" }
// Extract actual filter data for display - handles both columnFilters and OR filters in globalFilter const displayFilters = useMemo(() => { // For OR logic: filters are stored in globalFilter object if ( typeof globalFilter === "object" && globalFilter && "filters" in globalFilter ) { const filterObj = globalFilter as { filters: unknown[] joinOperator: string } return filterObj.filters || [] }
// For AND logic: filters should be in columnFilters return columnFilters }, [columnFilters, globalFilter])
// Enhanced filter statistics for better insights const filterStats = useMemo(() => { // For mixed logic stored in globalFilter object if ( typeof globalFilter === "object" && globalFilter && "filters" in globalFilter ) { const filterObj = globalFilter as { filters: Array<{ joinOperator?: string value?: unknown }> joinOperator: string } const filters = filterObj.filters || []
const hasAndFilters = filters.some( (filter, index) => index === 0 || filter.joinOperator === JOIN_OPERATORS.AND, ) const hasOrFilters = filters.some( (filter, index) => index > 0 && filter.joinOperator === JOIN_OPERATORS.OR, )
return { totalFilters: filters.length, hasAndFilters, hasOrFilters, effectiveJoinOperator: hasOrFilters ? JOIN_OPERATORS.MIXED : JOIN_OPERATORS.AND, activeFilters: filters.filter(f => f.value && f.value !== "").length, } }
// For regular columnFilters (AND logic) const hasAndFilters = columnFilters.length > 0 const hasOrFilters = columnFilters.some( filter => typeof filter.value === "object" && filter.value && "joinOperator" in filter.value && filter.value.joinOperator === "or", )
return { totalFilters: columnFilters.length, hasAndFilters, hasOrFilters, effectiveJoinOperator: hasOrFilters ? JOIN_OPERATORS.MIXED : JOIN_OPERATORS.AND, activeFilters: columnFilters.filter(f => f.value && f.value !== "") .length, } }, [columnFilters, globalFilter])
// Get current filter mode (AND, OR, or MIXED) const getFilterMode = () => { if ( typeof globalFilter === "object" && globalFilter && "filters" in globalFilter ) { const filterObj = globalFilter as { filters: unknown[] joinOperator: string } if (filterObj.joinOperator === "mixed") { return "MIXED" } return filterObj.joinOperator.toUpperCase() }
// Check if columnFilters have mixed join operators const hasOrOperators = columnFilters.some( filter => typeof filter.value === "object" && filter.value && "joinOperator" in filter.value && filter.value.joinOperator === "or", )
return hasOrOperators ? "MIXED" : "AND" }
// Memoized handlers for better performance const handleGlobalFilterChange = useCallback((value: string | object) => { setGlobalFilter(value) setPagination(prev => ({ ...prev, pageIndex: 0 })) }, [])
const handleSortingChange = useCallback((updater: Updater<SortingState>) => { setSorting(updater) }, [])
const handleColumnFiltersChange = useCallback( (updater: Updater<ColumnFiltersState>) => { setColumnFilters(updater) }, [], )
const handleColumnVisibilityChange = useCallback( (updater: Updater<VisibilityState>) => { setColumnVisibility(updater) }, [], )
const handlePaginationChange = useCallback( (updater: Updater<PaginationState>) => { setPagination(updater) }, [], )
// Extract ExtendedColumnFilter from globalFilter or columnFilters const currentFilters = useMemo(() => { // Check if filters are in globalFilter (OR/MIXED logic) if ( typeof globalFilter === "object" && globalFilter && "filters" in globalFilter ) { const filterObj = globalFilter as { filters: ExtendedColumnFilter<Product>[] } return filterObj.filters || [] }
// Otherwise extract from columnFilters (AND logic) return columnFilters .map(cf => cf.value) .filter( (v): v is ExtendedColumnFilter<Product> => v !== null && typeof v === "object" && "id" in v, ) }, [globalFilter, columnFilters])
// Handler for filter menu - converts between ExtendedColumnFilter and table state // Uses the core utility function for consistent behavior const handleFiltersChange = useCallback( (filters: ExtendedColumnFilter<Product>[] | null) => { if (!filters || filters.length === 0) { // Clear all filters setColumnFilters([]) setGlobalFilter("") setPagination(prev => ({ ...prev, pageIndex: 0 })) } else { // Use core utility to process filters and determine routing const result = processFiltersForLogic(filters)
if (result.shouldUseGlobalFilter) { // Use globalFilter for OR/MIXED logic setColumnFilters([]) setGlobalFilter({ filters: result.processedFilters, joinOperator: result.joinOperator, }) setPagination(prev => ({ ...prev, pageIndex: 0 })) } else { // Use columnFilters for AND logic setGlobalFilter("") setColumnFilters( result.processedFilters.map(filter => ({ id: filter.id, value: filter, })), ) setPagination(prev => ({ ...prev, pageIndex: 0 })) } } }, [], )
return ( <div className="w-full space-y-4"> <DataTableRoot data={data} columns={columns} // Pass state to DataTableRoot for controlled components state={{ globalFilter, sorting, columnFilters, columnVisibility, columnPinning, pagination, }} // Pass state updaters (using useCallback for performance) onGlobalFilterChange={handleGlobalFilterChange} onSortingChange={handleSortingChange} onColumnFiltersChange={handleColumnFiltersChange} onColumnVisibilityChange={handleColumnVisibilityChange} onPaginationChange={handlePaginationChange} onColumnPinningChange={setColumnPinning} > <FilterToolbar filters={currentFilters} onFiltersChange={handleFiltersChange} /> <DataTable> <DataTableHeader /> <DataTableBody> <DataTableEmptyBody> <DataTableEmptyMessage> <DataTableEmptyIcon> <UserSearch className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No customers found</DataTableEmptyTitle> <DataTableEmptyDescription> There are no customers to display at this time. </DataTableEmptyDescription> </DataTableEmptyMessage> <DataTableEmptyFilteredMessage> <DataTableEmptyIcon> <SearchX className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No matches found</DataTableEmptyTitle> <DataTableEmptyDescription> Try adjusting your filters or search to find what you're looking for. </DataTableEmptyDescription> </DataTableEmptyFilteredMessage> </DataTableEmptyBody> </DataTableBody> </DataTable> <DataTablePagination /> </DataTableRoot>
{/* State Display for demonstration */} <Card> <CardHeader> <CardTitle>Current Table State</CardTitle> <CardDescription> Live view of the current table state for demonstration purposes </CardDescription> <CardAction> <Button variant="outline" size="sm" onClick={resetAllState}> Reset All State </Button> </CardAction> </CardHeader> <CardContent className="space-y-4"> <div className="grid gap-2 text-xs text-muted-foreground"> <div className="flex justify-between"> <span className="font-medium">Search Query:</span> <span className="text-foreground"> {getGlobalFilterDisplay()} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Total Items:</span> <span className="text-foreground">{data.length}</span> </div>
<div className="flex justify-between"> <span className="font-medium">Active Filters:</span> <span className="text-foreground">{columnFilters.length}</span> </div>
<div className="flex justify-between"> <span className="font-medium">Enhanced Filters:</span> <span className="text-foreground"> {filterStats.totalFilters} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Active Enhanced:</span> <span className="text-foreground"> {filterStats.activeFilters} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Join Logic:</span> <span className="text-foreground"> {filterStats.effectiveJoinOperator} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Sorting:</span> <span className="text-foreground"> {sorting.length > 0 ? sorting .map(s => `${s.id} ${s.desc ? "desc" : "asc"}`) .join(", ") : "None"} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Page:</span> <span className="text-foreground"> {pagination.pageIndex + 1} (Size: {pagination.pageSize}) </span> </div>
<div className="flex justify-between"> <span className="font-medium">Hidden Columns:</span> <span className="text-foreground"> { Object.values(columnVisibility).filter(v => v === false) .length } </span> </div>
<div className="flex justify-between"> <span className="font-medium">Pinned Columns:</span> <span className="text-foreground"> {columnPinning.left?.length || 0} Left,{" "} {columnPinning.right?.length || 0} Right </span> </div> </div>
{/* Detailed state (collapsible) */} <details className="border-t pt-4"> <summary className="cursor-pointer text-xs font-medium hover:text-foreground"> View Full State Object </summary> <div className="mt-4 space-y-3 text-xs"> <div> <strong>Enhanced Filters:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {displayFilters.length > 0 ? JSON.stringify(displayFilters, null, 2) : "No enhanced filters"} </pre> </div> <div> <strong>Column Pinning:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(columnPinning, null, 2)} </pre> </div> <div> <strong>Filter Stats:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(filterStats, null, 2)} </pre> </div> <div> <strong>Filter Mode:</strong> {getFilterMode()} <div className="mt-1 text-muted-foreground"> {getFilterMode() === "AND" ? "All conditions must match (stored in columnFilters)" : getFilterMode() === "OR" ? "Any condition can match (stored in globalFilter)" : "Mixed logic - individual AND/OR operators per filter"} </div> </div> <div> <strong>Sorting:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(sorting, null, 2)} </pre> </div> <div> <strong>Column Filters State (AND logic):</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(columnFilters, null, 2)} </pre> </div> <div> <strong>Global Filter State (OR logic):</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(globalFilter, null, 2)} </pre> </div> <div> <strong>Column Visibility:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(columnVisibility, null, 2)} </pre> </div> </div> </details> </CardContent> </Card> </div> )}Introduction
Section titled “Introduction”The Advanced Table demonstrates rule-based filtering with DataTableFilterMenu and DataTableSortMenu for complex filtering scenarios. Users can create multiple filter rules, combine them with AND/OR operators, and manage sorting with a visual interface.
Installation
Section titled “Installation”- Add the required components:
npx shadcn@latest add table input button dropdown-menu popover command checkbox select scroll-area separator skeleton tooltip- Add
tanstack/react-tabledependency:
npm install @tanstack/react-tableRequired: Sortable Component
This example uses DataTableSortMenu and DataTableFilterMenu which require the Sortable component for drag-and-drop reordering. Follow the DiceUI Sortable installation guide.
- Copy the DataTable components into your project. See the Installation Guide for detailed instructions.
Prerequisites
Section titled “Prerequisites”We are going to build a table to show products with advanced filtering. Here’s what our data looks like:
type Product = { id: string name: string category: string brand: string price: number stock: number rating: number inStock: boolean releaseDate: Date}
const categoryOptions = [ { label: "Electronics", value: "electronics" }, { label: "Clothing", value: "clothing" }, { label: "Sports", value: "sports" },]
// Brand options can be auto-generated from data using meta.autoOptionsBasic Table with Advanced Filters
Section titled “Basic Table with Advanced Filters”Let’s start by building a table with rule-based filtering.
Column Definitions
Section titled “Column Definitions”First, we’ll define our columns with filter metadata.
"use client"
import { DataTableColumnHeader, DataTableColumnTitle, DataTableColumnSortMenu,} from "@/components/niko-table/components"import type { DataTableColumnDef } from "@/components/niko-table/types"
export type Product = { id: string name: string category: string brand: string price: number stock: number rating: number inStock: boolean releaseDate: Date}
const categoryOptions = [ { label: "Electronics", value: "electronics" }, { label: "Clothing", value: "clothing" }, { label: "Sports", value: "sports" },]
const brandOptions = [ { label: "Apple", value: "apple" }, { label: "Samsung", value: "samsung" }, { label: "Nike", value: "nike" },]
export const columns: DataTableColumnDef<Product>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Product Name", variant: "text", }, enableColumnFilter: true, }, { accessorKey: "category", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Category", variant: "select", options: categoryOptions, mergeStrategy: "augment", dynamicCounts: true, showCounts: true, }, enableColumnFilter: true, }, { accessorKey: "brand", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Brand", variant: "select", autoOptions: true, dynamicCounts: true, showCounts: true, }, enableColumnFilter: true, }, { accessorKey: "price", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Price", variant: "number", unit: "$", }, enableColumnFilter: true, },]<DataTable /> component
Section titled “<DataTable /> component”Next, we’ll add the filter and sort menu components.
"use client"
import { DataTableRoot, DataTable, DataTableHeader, DataTableBody, DataTableEmptyBody,} from "@/components/niko-table/core"import { DataTableToolbarSection, DataTablePagination, DataTableSearchFilter, DataTableViewMenu, DataTableSortMenu, DataTableFilterMenu, DataTableColumnHeader, DataTableColumnTitle, DataTableColumnSortMenu,} from "@/components/niko-table/components"import type { DataTableColumnDef } from "@/components/niko-table/types"
type Product = { id: string name: string category: string brand: string price: number}
const columns: DataTableColumnDef<Product>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Product Name", variant: "text", }, enableColumnFilter: true, }, { accessorKey: "category", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Category", variant: "select", options: [ { label: "Electronics", value: "electronics" }, { label: "Clothing", value: "clothing" }, ], }, enableColumnFilter: true, }, { accessorKey: "price", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Price", variant: "number", unit: "$", }, enableColumnFilter: true, },]
function FilterToolbar() { return ( <DataTableToolbarSection> <DataTableToolbarSection className="px-0"> <DataTableSearchFilter placeholder="Search products..." /> <DataTableViewMenu /> </DataTableToolbarSection> <DataTableToolbarSection className="px-0"> <DataTableSortMenu className="ml-auto" /> <DataTableFilterMenu autoOptions dynamicCounts showCounts mergeStrategy="augment" /> </DataTableToolbarSection> </DataTableToolbarSection> )}
export function AdvancedTable({ data }: { data: Product[] }) { return ( <DataTableRoot data={data} columns={columns} config={{ enablePagination: true, enableSorting: true, enableMultiSort: true, enableFilters: true, }} > <FilterToolbar /> <DataTable> <DataTableHeader /> <DataTableBody> <DataTableEmptyBody /> </DataTableBody> </DataTable> <DataTablePagination /> </DataTableRoot> )}Filter Components
Section titled “Filter Components”DataTableFilterMenu
Section titled “DataTableFilterMenu”The filter menu component allows users to create rule-based filters with multiple conditions.
Props:
| Name | Type | Default | Description |
|---|---|---|---|
autoOptions | boolean | true | Auto-generate options for select/multiSelect columns |
showCounts | boolean | true | Show counts beside each option |
dynamicCounts | boolean | true | Recompute counts based on currently filtered rows |
limitToFilteredRows | boolean | true | Generate options from filtered rows only (vs all rows) |
includeColumns | string[] | - | Only generate options for these column ids |
excludeColumns | string[] | - | Exclude these column ids from generation |
limitPerColumn | number | - | Limit number of generated options per column |
mergeStrategy | "preserve" | "augment" | "replace" | "preserve" | How to handle existing static options |
filters? | ExtendedColumnFilter[] | - | Array of filters (for controlled mode) |
onFiltersChange? | (filters: ExtendedColumnFilter[] | null) => void | - | Callback when filters change |
className | string | - | Additional CSS classes |
Merge Strategy:
preserve: Keep user-defined options untouched (default)augment: Add counts to matching values from static optionsreplace: Override static options with generated options
<DataTableFilterMenu autoOptions dynamicCounts showCounts limitToFilteredRows={true} mergeStrategy="augment"/>DataTableSortMenu
Section titled “DataTableSortMenu”The sort menu component provides a visual interface for managing column sorting.
Props:
| Name | Type | Description |
|---|---|---|
className | string | Additional CSS classes |
<DataTableSortMenu className="ml-auto" />Column Meta for Filters
Section titled “Column Meta for Filters”Define filter metadata in your column definitions:
{ accessorKey: "category", meta: { label: "Category", variant: "select", options: [ { label: "Electronics", value: "electronics" }, { label: "Clothing", value: "clothing" }, ], }, enableColumnFilter: true,}Supported variants:
"text"- Text input filter"select"- Dropdown select filter (requiresoptions)"multiSelect"- Multiple selection dropdown"number"- Number input filter (optionalunitfor display)"range"- Numeric range slider (auto-applied fornumbervariant withmeta.range)"boolean"- Boolean checkbox filter"date"- Single date picker filter"date_range"- Date range picker (auto-applied whenDataTableDateFilterhasmultiple={true})
Auto-applied filter functions:
When you specify meta.variant in your column definition, the appropriate filterFn is automatically applied:
variant: "range"→numberRangeFilter(for numeric ranges like price)variant: "date"orvariant: "date_range"→dateRangeFilter(for date filtering)
You can still provide a custom filterFn if needed - it will override the auto-applied function.
Controlled State
Section titled “Controlled State”Full control over table state:
import { useState } from "react"import type { PaginationState, SortingState, ColumnFiltersState,} from "@tanstack/react-table"import type { ExtendedColumnFilter } from "@/components/niko-table/types"
export function ControlledAdvancedTable({ data }: { data: Product[] }) { const [globalFilter, setGlobalFilter] = useState<string | object>("") const [sorting, setSorting] = useState<SortingState>([]) const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]) const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 10, })
const handleFiltersChange = ( filters: ExtendedColumnFilter<Product>[] | null, ) => { if (!filters || filters.length === 0) { setColumnFilters([]) } else { setColumnFilters( filters.map(filter => ({ id: filter.id, value: filter, })), ) } }
return ( <DataTableRoot data={data} columns={columns} config={{ enablePagination: true, enableSorting: true, enableMultiSort: true, enableFilters: true, }} state={{ globalFilter, sorting, columnFilters, pagination, }} onGlobalFilterChange={setGlobalFilter} onSortingChange={setSorting} onColumnFiltersChange={setColumnFilters} onPaginationChange={setPagination} > <DataTableToolbarSection> <DataTableToolbarSection className="px-0"> <DataTableSearchFilter placeholder="Search products..." /> <DataTableViewMenu /> </DataTableToolbarSection> <DataTableToolbarSection className="px-0"> <DataTableSortMenu className="ml-auto" /> <DataTableFilterMenu filters={[]} onFiltersChange={handleFiltersChange} /> </DataTableToolbarSection> </DataTableToolbarSection> <DataTable> <DataTableHeader /> <DataTableBody> <DataTableEmptyBody /> </DataTableBody> </DataTable> <DataTablePagination /> </DataTableRoot> )}When to Use
Section titled “When to Use”✅ Use Advanced Table when:
- Users need complex, rule-based filtering
- You want visual filter and sort management
- Multiple filter conditions need to be combined
- You need drag-and-drop sorting interface
- Filter state needs to be managed externally
❌ Consider other options when:
- Simple filtering is sufficient (use Faceted Filter Table)
- You prefer inline filters (use Advanced Inline Filter Table)
- You need URL state persistence (use Advanced Nuqs Table)
Next Steps
Section titled “Next Steps”- Advanced Inline Filter Table - Build custom inline filter UIs
- Advanced Nuqs Table - Add URL state persistence