Skip to content

Faceted Filter Table

Add inline faceted filters that show available options and counts.

Open in
Preview with Controlled State
Open in

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.

  1. Add the required components:
npx shadcn@latest add table input button dropdown-menu popover command badge scroll-area separator
  1. Add tanstack/react-table dependency:
npm install @tanstack/react-table
  1. Copy the DataTable components into your project. See the Installation Guide for detailed instructions.

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

Next, we’ll add faceted filter components.

faceted-filter-table.tsx
"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>
)
}

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: "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!
}

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: