Skip to content

Faceted Filter Table

Add inline faceted filters that show available options and counts.

Open in
"use client"
import { DataTableRoot } from "@/components/niko-table/core/data-table-root"
import { DataTable } from "@/components/niko-table/core/data-table"
import {
DataTableHeader,
DataTableBody,
DataTableEmptyBody,
} from "@/components/niko-table/core/data-table-structure"
import { DataTableColumnHeader } from "@/components/niko-table/components/data-table-column-header"
import { DataTableColumnTitle } from "@/components/niko-table/components/data-table-column-title"
import { DataTableColumnSortMenu } from "@/components/niko-table/components/data-table-column-sort"
import { DataTableColumnFacetedFilterMenu } from "@/components/niko-table/components/data-table-column-faceted-filter"
import { DataTableColumnSliderFilterMenu } from "@/components/niko-table/components/data-table-column-slider-filter-options"
import { DataTableColumnDateFilterMenu } from "@/components/niko-table/components/data-table-column-date-filter-options"
import { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"
import {
DataTableEmptyIcon,
DataTableEmptyMessage,
DataTableEmptyFilteredMessage,
DataTableEmptyTitle,
DataTableEmptyDescription,
} from "@/components/niko-table/components/data-table-empty-state"
import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"
import { DataTableViewMenu } from "@/components/niko-table/components/data-table-view-menu"
import { DataTableFacetedFilter } from "@/components/niko-table/components/data-table-faceted-filter"
import { DataTableClearFilter } from "@/components/niko-table/components/data-table-clear-filter"
import { DataTableSliderFilter } from "@/components/niko-table/components/data-table-slider-filter"
import { DataTableDateFilter } from "@/components/niko-table/components/data-table-date-filter"
import { DataTablePagination } from "@/components/niko-table/components/data-table-pagination"
import { daysAgo } from "@/components/niko-table/lib/format"
import { FILTER_VARIANTS } from "@/components/niko-table/lib/constants"
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} />
{/*
* Multi-select relies on the new `!multiple` default: `limitToFilteredRows`
* resolves to `false` automatically, so the full option universe stays
* visible as the user selects/deselects values. No explicit prop needed.
*/}
<DataTableColumnFacetedFilterMenu multiple />
</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: "dateRange", // 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: "dateRange" -> 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&apos;re
looking for.
</DataTableEmptyDescription>
</DataTableEmptyFilteredMessage>
</DataTableEmptyBody>
</DataTableBody>
</DataTable>
<DataTablePagination />
</DataTableRoot>
)
}
Preview with Controlled State
Open in
"use client"
import { useState } from "react"
import type {
PaginationState,
SortingState,
ColumnFiltersState,
VisibilityState,
ColumnPinningState,
} from "@tanstack/react-table"
import { DataTableRoot } from "@/components/niko-table/core/data-table-root"
import { DataTable } from "@/components/niko-table/core/data-table"
import {
DataTableHeader,
DataTableBody,
DataTableEmptyBody,
} from "@/components/niko-table/core/data-table-structure"
import { DataTableColumnHeader } from "@/components/niko-table/components/data-table-column-header"
import { DataTableColumnTitle } from "@/components/niko-table/components/data-table-column-title"
import { DataTableColumnActions } from "@/components/niko-table/components/data-table-column-actions"
import { DataTableColumnFilter } from "@/components/niko-table/components/data-table-column-filter"
import { DataTableColumnFilterTrigger } from "@/components/niko-table/components/data-table-column-filter-trigger"
import {
DataTableColumnSortMenu,
DataTableColumnSortOptions,
} from "@/components/niko-table/components/data-table-column-sort"
import { DataTableColumnFacetedFilterMenu } from "@/components/niko-table/components/data-table-column-faceted-filter"
import { DataTableColumnSliderFilterMenu } from "@/components/niko-table/components/data-table-column-slider-filter-options"
import { DataTableColumnDateFilterMenu } from "@/components/niko-table/components/data-table-column-date-filter-options"
import { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"
import {
DataTableEmptyIcon,
DataTableEmptyMessage,
DataTableEmptyFilteredMessage,
DataTableEmptyTitle,
DataTableEmptyDescription,
} from "@/components/niko-table/components/data-table-empty-state"
import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"
import { DataTableViewMenu } from "@/components/niko-table/components/data-table-view-menu"
import { DataTableFacetedFilter } from "@/components/niko-table/components/data-table-faceted-filter"
import { DataTableClearFilter } from "@/components/niko-table/components/data-table-clear-filter"
import { DataTableSliderFilter } from "@/components/niko-table/components/data-table-slider-filter"
import { DataTableDateFilter } from "@/components/niko-table/components/data-table-date-filter"
import { DataTablePagination } from "@/components/niko-table/components/data-table-pagination"
import { daysAgo } from "@/components/niko-table/lib/format"
import { FILTER_VARIANTS } from "@/components/niko-table/lib/constants"
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&apos;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>
)
}

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.

Install the DataTable core and add-ons for this example:

pnpm dlx shadcn@latest add @niko-table/data-table @niko-table/data-table-pagination @niko-table/data-table-search-filter @niko-table/data-table-view-menu @niko-table/data-table-faceted-filter @niko-table/data-table-clear-filter @niko-table/data-table-slider-filter @niko-table/data-table-date-filter @niko-table/data-table-column-sort @niko-table/data-table-column-faceted-filter @niko-table/data-table-column-slider-filter @niko-table/data-table-column-date-filter

First time using @niko-table? See the Installation Guide to set up the registry.

For other add-ons or manual copy-paste, see the Installation Guide.

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" },
]

Let’s start by building a table with faceted filters.

First, we’ll define our columns with filter functions.

columns.tsx
"use client"
import {
DataTableColumnHeader,
DataTableColumnTitle,
} from "@/components/niko-table/components/data-table-column-header"
import { DataTableColumnSortMenu } from "@/components/niko-table/components/data-table-column-sort"
import { DataTableColumnFacetedFilterMenu } from "@/components/niko-table/components/data-table-column-faceted-filter"
import { DataTableColumnSliderFilterMenu } from "@/components/niko-table/components/data-table-column-slider-filter-options"
import { DataTableColumnDateFilterMenu } from "@/components/niko-table/components/data-table-column-date-filter-options"
import { FILTER_VARIANTS } from "@/components/niko-table/lib/constants"
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: "dateRange", // Auto-applies dateRangeFilter
},
enableColumnFilter: true,
},
]

Next, we’ll add faceted filter components.

faceted-filter-table.tsx
"use client"
import { DataTableRoot } from "@/components/niko-table/core/data-table-root"
import { DataTable } from "@/components/niko-table/core/data-table"
import {
DataTableHeader,
DataTableBody,
DataTableEmptyBody,
} from "@/components/niko-table/core/data-table-structure"
import { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"
import { DataTablePagination } from "@/components/niko-table/components/data-table-pagination"
import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"
import { DataTableViewMenu } from "@/components/niko-table/components/data-table-view-menu"
import { DataTableFacetedFilter } from "@/components/niko-table/components/data-table-faceted-filter"
import { DataTableSliderFilter } from "@/components/niko-table/components/data-table-slider-filter"
import { DataTableDateFilter } from "@/components/niko-table/components/data-table-date-filter"
import { DataTableClearFilter } from "@/components/niko-table/components/data-table-clear-filter"
import {
DataTableColumnHeader,
DataTableColumnTitle,
} from "@/components/niko-table/components/data-table-column-header"
import { DataTableColumnSortMenu } from "@/components/niko-table/components/data-table-column-sort"
import { DataTableColumnFacetedFilterMenu } from "@/components/niko-table/components/data-table-column-faceted-filter"
import { DataTableColumnSliderFilterMenu } from "@/components/niko-table/components/data-table-column-slider-filter-options"
import { DataTableColumnDateFilterMenu } from "@/components/niko-table/components/data-table-column-date-filter-options"
import { FILTER_VARIANTS } from "@/components/niko-table/lib/constants"
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: "dateRange",
},
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>
)
}

The faceted filter component shows available options with counts.

Props:

NameTypeDefaultDescription
accessorKeykeyof TData & string-The column key to filter (required)
titlestring-Optional override (defaults to column meta.label or accessor name)
optionsOption[]-Static options; if omitted and column meta allows, options are auto-generated
multipleboolean-Allow multiple selections (defaults based on column variant)
showCountsbooleantrueShow counts next to options
dynamicCountsbooleantrueCounts reflect filtered rows
limitToFilteredRowsbooleantrueShow only options from filtered rows (vs all rows)

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 />

Button to clear all active filters.

<DataTableClearFilter />

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" or variant: "dateRange"dateRangeFilter (for date filtering)
{
accessorKey: "price",
meta: {
label: "Price",
variant: "range", // Auto-applies numberRangeFilter
unit: "$",
},
// No need to define filterFn - it's auto-applied!
}

Enable multiple selections with the multiple prop:

<DataTableFacetedFilter
accessorKey="category"
title="Category"
options={categoryOptions}
multiple // Allow selecting multiple categories
/>

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>
)
}

✅ 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: