Faceted Filter Table
Add inline faceted filters that show available options and counts.
"use client"
import { DataTableRoot, DataTable, DataTableHeader, DataTableBody, DataTableEmptyBody,} from "@/components/niko-table/core"import { DataTableToolbarSection, DataTablePagination, DataTableSearchFilter, DataTableViewMenu, DataTableFacetedFilter, DataTableClearFilter, DataTableSliderFilter, DataTableDateFilter, DataTableEmptyFilteredMessage, DataTableEmptyTitle, DataTableEmptyDescription, DataTableEmptyIcon, DataTableEmptyMessage, 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" },]
const columns: DataTableColumnDef<Product>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Product Name", }, }, { accessorKey: "category", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> <DataTableColumnFacetedFilterMenu multiple limitToFilteredRows={false} /> </DataTableColumnHeader> ), meta: { label: "Category", 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 limitToFilteredRows /> </DataTableColumnHeader> ), meta: { label: "Brand", 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: "range", // Auto-applies numberRangeFilter }, cell: ({ row }) => { const price = parseFloat(row.getValue("price")) return <div className="font-medium">${price.toFixed(2)}</div> }, enableColumnFilter: true, // filterFn auto-applied based on variant: "range" -> numberRangeFilter }, { accessorKey: "stock", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.NUMBER} /> </DataTableColumnHeader> ), meta: { label: "Stock", }, cell: ({ row }) => { const stock = row.getValue("stock") as number return ( <div className={stock < 10 ? "font-medium text-red-600" : ""}> {stock} </div> ) }, }, { accessorKey: "rating", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.NUMBER} /> </DataTableColumnHeader> ), meta: { label: "Rating", autoOptions: true, dynamicCounts: true, }, cell: ({ row }) => { const rating = row.getValue("rating") as number 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", options: [ { label: "In Stock", value: "true" }, { label: "Out of Stock", value: "false" }, ], mergeStrategy: "preserve", // keep static labels, no counts needed here }, cell: ({ row }) => { const inStock = row.getValue("inStock") as boolean 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: "date_range", // Auto-applies dateRangeFilter }, cell: ({ row }) => { const date = row.getValue("releaseDate") as Date return <span>{date.toLocaleDateString()}</span> }, enableColumnFilter: true, // filterFn auto-applied based on variant: "date_range" -> dateRangeFilter },]
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 className="w-full flex-col justify-between gap-2"> <DataTableToolbarSection className="px-0"> <DataTableSearchFilter placeholder="Search products..." /> <DataTableViewMenu /> </DataTableToolbarSection> <DataTableToolbarSection className="px-0"> {/* Category: static list + live counts (augment) - show all options from entire dataset */} <DataTableFacetedFilter accessorKey="category" multiple limitToFilteredRows={false} /> {/* Brand: fully generated options - show only options in filtered rows */} <DataTableFacetedFilter accessorKey="brand" limitToFilteredRows /> {/* Rating: auto-generated (numbers become categorical) - show only options in filtered rows */} <DataTableFacetedFilter accessorKey="rating" limitToFilteredRows /> {/* In Stock: preserve static options (no counts) - show only options in filtered rows */} <DataTableFacetedFilter accessorKey="inStock" limitToFilteredRows /> <DataTableSliderFilter accessorKey="price" /> <DataTableDateFilter accessorKey="releaseDate" multiple /> <DataTableClearFilter /> </DataTableToolbarSection> </DataTableToolbarSection> )}
export default function FacetedTableExample() { 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
[]
[]
{}{
"left": [],
"right": []
}"use client"
import { useState } from "react"import type { PaginationState, SortingState, ColumnFiltersState, VisibilityState, ColumnPinningState,} from "@tanstack/react-table"import { DataTableRoot, DataTable, DataTableHeader, DataTableBody, DataTableEmptyBody,} from "@/components/niko-table/core"import { DataTableToolbarSection, DataTablePagination, DataTableSearchFilter, DataTableViewMenu, DataTableFacetedFilter, DataTableClearFilter, DataTableSliderFilter, DataTableDateFilter, DataTableEmptyFilteredMessage, DataTableEmptyTitle, DataTableEmptyDescription, DataTableEmptyIcon, DataTableEmptyMessage, DataTableColumnHeader, DataTableColumnTitle, DataTableColumnActions, DataTableColumnSortMenu, DataTableColumnSortOptions, DataTableColumnFilter, DataTableColumnFilterTrigger, 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, Filter } from "lucide-react"import { Button } from "@/components/ui/button"import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card"
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 columns: DataTableColumnDef<Product>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader className="justify-start"> <span className="mr-2 text-sm font-semibold">Product Name</span> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Product Name", }, }, { accessorKey: "category", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnActions> <div className="border-b p-2"> <DataTableFacetedFilter accessorKey="category" options={categoryOptions} trigger={ <Button variant="ghost" size="sm" className="h-8 w-full justify-start font-normal" > <Filter className="mr-2 h-4 w-4" /> Filter </Button> } /> </div> <DataTableColumnSortOptions variant={FILTER_VARIANTS.TEXT} /> </DataTableColumnActions> </DataTableColumnHeader> ), meta: { label: "Category", 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 /> <DataTableColumnActions> <DataTableColumnFilter> <DataTableFacetedFilter accessorKey="brand" trigger={<DataTableColumnFilterTrigger />} /> </DataTableColumnFilter> <DataTableColumnSortOptions variant={FILTER_VARIANTS.TEXT} /> </DataTableColumnActions> </DataTableColumnHeader> ), meta: { label: "Brand", autoOptions: true, dynamicCounts: true, showCounts: true, }, enableColumnFilter: true, }, { accessorKey: "price", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.NUMBER} /> <DataTableColumnSliderFilterMenu /> </DataTableColumnHeader> ), meta: { label: "Price", unit: "$", }, cell: ({ row }) => { const price = parseFloat(row.getValue("price")) return <div className="font-medium">${price.toFixed(2)}</div> }, enableColumnFilter: true, filterFn: (row, id, filterValue: [number, number]) => { if (!filterValue) return true const value = row.getValue(id) as number return value >= filterValue[0] && value <= filterValue[1] }, }, { accessorKey: "stock", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.NUMBER} /> </DataTableColumnHeader> ), meta: { label: "Stock", }, cell: ({ row }) => { const stock = row.getValue("stock") as number return ( <div className={stock < 10 ? "font-medium text-red-600" : ""}> {stock} </div> ) }, }, { accessorKey: "rating", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.NUMBER} /> </DataTableColumnHeader> ), meta: { label: "Rating", autoOptions: true, dynamicCounts: true, }, cell: ({ row }) => { const rating = row.getValue("rating") as number 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", options: [ { label: "In Stock", value: "true" }, { label: "Out of Stock", value: "false" }, ], mergeStrategy: "preserve", }, cell: ({ row }) => { const inStock = row.getValue("inStock") as boolean return ( <Badge variant={inStock ? "default" : "secondary"}> {inStock ? "Yes" : "No"} </Badge> ) }, enableColumnFilter: true, }, { accessorKey: "releaseDate", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> <DataTableColumnDateFilterMenu /> </DataTableColumnHeader> ), meta: { label: "Release Date", }, cell: ({ row }) => { const date = row.getValue("releaseDate") as Date return <span>{date.toLocaleDateString()}</span> }, enableColumnFilter: true, filterFn: (row, id, filterValue: number | [number, number]) => { if (!filterValue) return true const rowValue = (row.getValue(id) as Date).getTime()
if (Array.isArray(filterValue)) { const [from, to] = filterValue if (from && to) { return rowValue >= from && rowValue <= to } if (from) return rowValue >= from if (to) return rowValue <= to return true }
return rowValue === filterValue }, },]
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() { return ( <DataTableToolbarSection className="w-full flex-col justify-between gap-2"> <DataTableToolbarSection className="px-0"> <DataTableSearchFilter placeholder="Search products..." /> <DataTableViewMenu /> </DataTableToolbarSection> <DataTableToolbarSection className="px-0"> {/* Category: static list + live counts (augment) - show all options from entire dataset */} <DataTableFacetedFilter accessorKey="category" multiple limitToFilteredRows={false} /> {/* Brand: fully generated options - show only options in filtered rows */} <DataTableFacetedFilter accessorKey="brand" limitToFilteredRows /> {/* Rating: auto-generated (numbers become categorical) - show only options in filtered rows */} <DataTableFacetedFilter accessorKey="rating" limitToFilteredRows /> {/* In Stock: preserve static options (no counts) - show only options in filtered rows */} <DataTableFacetedFilter accessorKey="inStock" limitToFilteredRows /> <DataTableSliderFilter accessorKey="price" /> <DataTableDateFilter accessorKey="releaseDate" multiple /> <DataTableClearFilter /> </DataTableToolbarSection> </DataTableToolbarSection> )}
export default function FacetedStateTableExample() { // 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 = () => { setGlobalFilter("") setSorting([]) setColumnFilters([]) setColumnVisibility({}) setColumnPinning({ left: [], right: [] }) setPagination({ pageIndex: 0, pageSize: 10 }) }
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 onGlobalFilterChange={value => { setGlobalFilter(value) setPagination(prev => ({ ...prev, pageIndex: 0 })) }} onSortingChange={setSorting} onColumnFiltersChange={filters => { setColumnFilters(filters) setPagination(prev => ({ ...prev, pageIndex: 0 })) }} onColumnVisibilityChange={setColumnVisibility} onColumnPinningChange={setColumnPinning} onPaginationChange={setPagination} > <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>
{/* 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"> {typeof globalFilter === "string" ? globalFilter || "None" : "Mixed Filters"} </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">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>Column Filters:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(columnFilters, null, 2)} </pre> </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 Visibility:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(columnVisibility, null, 2)} </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> </details> </CardContent> </Card> </div> )}Introduction
Section titled “Introduction”The Faceted Filter Table adds inline filter components that show available options with counts. Users can quickly see what filters are available and how many items match each option.
Installation
Section titled “Installation”- Add the required components:
npx shadcn@latest add table input button dropdown-menu popover command badge scroll-area separator- Add
tanstack/react-tabledependency:
npm install @tanstack/react-table- 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 faceted filters. 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" },]Basic Table with Faceted Filters
Section titled “Basic Table with Faceted Filters”Let’s start by building a table with faceted filters.
Column Definitions
Section titled “Column Definitions”First, we’ll define our columns with filter functions.
"use client"
import { DataTableColumnHeader, DataTableColumnTitle, DataTableColumnSortMenu, DataTableColumnFacetedFilterMenu, DataTableColumnSliderFilterMenu, DataTableColumnDateFilterMenu,} from "@/components/niko-table/components"import { FILTER_VARIANTS } from "@/components/niko-table/lib"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}
export const columns: DataTableColumnDef<Product>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Product Name" }, }, { accessorKey: "category", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> <DataTableColumnFacetedFilterMenu multiple limitToFilteredRows={false} /> </DataTableColumnHeader> ), meta: { label: "Category", options: categoryOptions, mergeStrategy: "augment", dynamicCounts: true, showCounts: true, }, enableColumnFilter: true, }, { accessorKey: "brand", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> <DataTableColumnFacetedFilterMenu limitToFilteredRows /> </DataTableColumnHeader> ), meta: { label: "Brand", 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: "range", // Auto-applies numberRangeFilter }, enableColumnFilter: true, }, { accessorKey: "releaseDate", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> <DataTableColumnDateFilterMenu /> </DataTableColumnHeader> ), meta: { label: "Release Date", variant: "date_range", // Auto-applies dateRangeFilter }, enableColumnFilter: true, },]<DataTable /> component
Section titled “<DataTable /> component”Next, we’ll add faceted filter components.
"use client"
import { DataTableRoot, DataTable, DataTableHeader, DataTableBody, DataTableEmptyBody,} from "@/components/niko-table/core"import { DataTableToolbarSection, DataTablePagination, DataTableSearchFilter, DataTableViewMenu, DataTableFacetedFilter, DataTableSliderFilter, DataTableDateFilter, DataTableClearFilter, DataTableColumnHeader, DataTableColumnTitle, DataTableColumnSortMenu, DataTableColumnFacetedFilterMenu, DataTableColumnSliderFilterMenu, DataTableColumnDateFilterMenu,} from "@/components/niko-table/components"import { FILTER_VARIANTS } from "@/components/niko-table/lib"import type { DataTableColumnDef } from "@/components/niko-table/types"
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 columns: DataTableColumnDef<Product>[] = [ { accessorKey: "name", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> </DataTableColumnHeader> ), meta: { label: "Product Name" }, }, { accessorKey: "category", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> <DataTableColumnFacetedFilterMenu multiple limitToFilteredRows={false} /> </DataTableColumnHeader> ), meta: { label: "Category", options: categoryOptions, mergeStrategy: "augment", dynamicCounts: true, showCounts: true, }, enableColumnFilter: true, }, { accessorKey: "brand", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> <DataTableColumnFacetedFilterMenu limitToFilteredRows /> </DataTableColumnHeader> ), meta: { label: "Brand", 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: "range", }, enableColumnFilter: true, }, { accessorKey: "releaseDate", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu /> <DataTableColumnDateFilterMenu /> </DataTableColumnHeader> ), meta: { label: "Release Date", variant: "date_range", }, enableColumnFilter: true, },]
export function FacetedFilterTable({ data }: { data: Product[] }) { return ( <DataTableRoot data={data} columns={columns}> <DataTableToolbarSection className="w-full flex-col justify-between gap-2"> <DataTableToolbarSection className="px-0"> <DataTableSearchFilter placeholder="Search products..." /> <DataTableViewMenu /> </DataTableToolbarSection> <DataTableToolbarSection className="flex-wrap px-0"> {/* Category: show all options (not limited by other filters) */} <DataTableFacetedFilter accessorKey="category" multiple limitToFilteredRows={false} /> {/* Brand: show only brands from filtered rows */} <DataTableFacetedFilter accessorKey="brand" limitToFilteredRows /> <DataTableSliderFilter accessorKey="price" /> <DataTableDateFilter accessorKey="releaseDate" multiple /> <DataTableClearFilter /> </DataTableToolbarSection> </DataTableToolbarSection>
<DataTable> <DataTableHeader /> <DataTableBody> <DataTableEmptyBody /> </DataTableBody> </DataTable>
<DataTablePagination /> </DataTableRoot> )}Faceted Filter Component
Section titled “Faceted Filter Component”DataTableFacetedFilter
Section titled “DataTableFacetedFilter”The faceted filter component shows available options with counts.
Props:
| Name | Type | Default | Description |
|---|---|---|---|
accessorKey | keyof TData & string | - | The column key to filter (required) |
title | string | - | Optional override (defaults to column meta.label or accessor name) |
options | Option[] | - | Static options; if omitted and column meta allows, options are auto-generated |
multiple | boolean | - | Allow multiple selections (defaults based on column variant) |
showCounts | boolean | true | Show counts next to options |
dynamicCounts | boolean | true | Counts reflect filtered rows |
limitToFilteredRows | boolean | true | Show only options from filtered rows (vs all rows) |
limitToFilteredRows
Section titled “limitToFilteredRows”The limitToFilteredRows prop controls whether the filter shows options from all data or only from the currently filtered rows.
When limitToFilteredRows={true} (default):
- Options are generated from rows that match other active filters
- Useful for filters like “Brand” or “Rating” where you want to see only relevant options
- Example: If you filter by “Category: Electronics”, the Brand filter will only show brands that exist in Electronics
When limitToFilteredRows={false}:
- Options are generated from all rows, regardless of other filters
- Useful for filters like “Category” where you want to see all available categories
- Example: Category filter always shows all categories, even when other filters are active
// Category: Show all options (not limited by other filters)<DataTableFacetedFilter accessorKey="category" title="Category" options={categoryOptions} multiple limitToFilteredRows={false} // Always show all categories/>
// Brand: Show only brands from filtered rows<DataTableFacetedFilter accessorKey="brand" title="Brand" multiple limitToFilteredRows={true} // Only show brands from current filter results/>// Auto-generated when meta.autoOptions or meta.options are set appropriately<DataTableFacetedFilter accessorKey="category" multiple />DataTableClearFilter
Section titled “DataTableClearFilter”Button to clear all active filters.
<DataTableClearFilter />Filter Functions
Section titled “Filter Functions”Note: For most use cases, you don’t need to define custom filterFn. The default extendedFilter handles the ExtendedColumnFilter format automatically. Only define a custom filterFn if you need special filtering logic.
If you do define a custom filterFn, it will work as you define it:
{ accessorKey: "category", enableColumnFilter: true, // Custom filter function (optional - default works for most cases) filterFn: (row, id, filterValue: string[]) => { if (!filterValue?.length) return true const rowValue = String(row.getValue(id)) return filterValue.includes(rowValue) },}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)
{ accessorKey: "price", meta: { label: "Price", variant: "range", // Auto-applies numberRangeFilter unit: "$", }, // No need to define filterFn - it's auto-applied!}Multiple Selection
Section titled “Multiple Selection”Enable multiple selections with the multiple prop:
<DataTableFacetedFilter accessorKey="category" title="Category" options={categoryOptions} multiple // Allow selecting multiple categories/>Controlled State
Section titled “Controlled State”Manage filter state externally for full control:
import { useState } from "react"import type { PaginationState, SortingState, ColumnFiltersState, VisibilityState,} from "@tanstack/react-table"
export function ControlledFacetedFilterTable({ data }: { data: Product[] }) { 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, })
return ( <DataTableRoot data={data} columns={columns} state={{ globalFilter, sorting, columnFilters, columnVisibility, pagination, }} onGlobalFilterChange={value => { setGlobalFilter(value) setPagination(prev => ({ ...prev, pageIndex: 0 })) }} onSortingChange={setSorting} onColumnFiltersChange={filters => { setColumnFilters(filters) setPagination(prev => ({ ...prev, pageIndex: 0 })) }} onColumnVisibilityChange={setColumnVisibility} onPaginationChange={setPagination} > <DataTableToolbarSection className="w-full flex-col justify-between gap-2"> <DataTableToolbarSection className="px-0"> <DataTableSearchFilter placeholder="Search products..." /> <DataTableViewMenu /> </DataTableToolbarSection> <DataTableToolbarSection className="flex-wrap px-0"> <DataTableFacetedFilter accessorKey="category" multiple limitToFilteredRows={false} /> <DataTableFacetedFilter accessorKey="brand" limitToFilteredRows /> <DataTableSliderFilter accessorKey="price" /> <DataTableDateFilter accessorKey="releaseDate" multiple /> <DataTableClearFilter /> </DataTableToolbarSection> </DataTableToolbarSection>
<DataTable> <DataTableHeader /> <DataTableBody> <DataTableEmptyBody /> </DataTableBody> </DataTable>
<DataTablePagination /> </DataTableRoot> )}When to Use
Section titled “When to Use”✅ Use Faceted Filter Table when:
- You have categorical data (categories, tags, statuses)
- Users need to see available options and counts
- You want inline, visible filters
- Multiple filter options are common
❌ Consider other options when:
- You need complex rule-based filtering (use Advanced Table)
- You prefer dropdown filters (use Advanced Table)
- You only need search (use Search Table)
Next Steps
Section titled “Next Steps”- Advanced Table - Add advanced filtering menus
- Search Table - Add global search