Filtering
Compose DataTable filtering with the general Filters composite and TanStack adapters.
Overview
DataTable filtering is composed from normal Cubitt surfaces:
defineFilters(...)describes the filter fieldsFiltersrenders the UIuseFiltersTanstack(...)adapts those filters to TanStack TableDataTablereceives the adaptedcolumns,columnFilters, and change handlers
This is the same pattern as Search and Column Visibility: the table stays focused on rendering, while surrounding state and UI stay composable.
"use client";
import { DataTable } from "@tilt-legal/cubitt-components/data-table";
import {
defineFilters,
Filters,
useFiltersTanstack,
} from "@tilt-legal/cubitt-components/filters";
import { Badge } from "@tilt-legal/cubitt-components/badge";
import { ColumnDef } from "@tanstack/react-table";
const employees = [
{
id: "1",
name: "Alice Johnson",
email: "alice@example.com",
status: "Active",
department: "Engineering",
location: "New York",
},
{
id: "2",
name: "Michael Chen",
email: "michael@example.com",
status: "Active",
department: "Design",
location: "San Francisco",
},
{
id: "3",
name: "Priya Nair",
email: "priya@example.com",
status: "On Leave",
department: "Marketing",
location: "Toronto",
},
{
id: "4",
name: "Daniel Martinez",
email: "daniel@example.com",
status: "Inactive",
department: "Sales",
location: "Mexico City",
},
{
id: "5",
name: "Fatima Al-Sayed",
email: "fatima@example.com",
status: "Active",
department: "Product",
location: "Dubai",
},
];
type Employee = (typeof employees)[number];
const filterDefs = defineFilters([
{
key: "search",
type: "text",
label: "Search",
placeholder: "Search name, email, or department",
operators: [{ value: "contains", label: "Contains" }],
},
{
key: "status",
type: "select",
label: "Status",
operators: [
{ value: "is", label: "Is" },
{ value: "is_not", label: "Is not" },
],
},
{
key: "department",
type: "select",
label: "Department",
operators: [{ value: "is", label: "Is" }],
},
] as const);
const baseColumns: ColumnDef<Employee>[] = [
{
accessorKey: "name",
header: "Name",
cell: ({ row }) => <div>{row.getValue("name") as string}</div>,
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const status = row.getValue("status") as string;
let variant: "success" | "secondary" | "destructive";
if (status === "Active") variant = "success";
else if (status === "On Leave") variant = "secondary";
else variant = "destructive";
return <Badge variant={variant}>{status}</Badge>;
},
},
{
accessorKey: "department",
header: "Department",
},
{
accessorKey: "location",
header: "Location",
},
];
export function EmployeesTable() {
const {
filters,
handleFiltersChange,
filterFields,
columns,
columnFilters,
onColumnFiltersChange,
} = useFiltersTanstack<Employee, typeof filterDefs>({
definition: filterDefs,
columns: baseColumns,
data: employees,
fieldColumnMap: { search: "name" },
searchResolvers: {
search: (row) => [row.name, row.email, row.department],
},
});
return (
<div className="space-y-4">
<Filters
fields={filterFields}
filters={filters}
onChange={handleFiltersChange}
/>
<DataTable
columnFilters={columnFilters}
columns={columns}
data={employees}
enableFiltering
enableSorting
onColumnFiltersChange={onColumnFiltersChange}
/>
</div>
);
}How The Composition Works
useFiltersTanstack(...) gives you the pieces that bridge Filters and DataTable:
filtersandhandleFiltersChangefor theFilterscomponentfilterFieldswhich may differ from the original definition after runtime option resolutioncolumns,columnFilters, andonColumnFiltersChangeforDataTable
That means the recommended flow is:
- define fields with
defineFilters(...) - pass the definition plus your table columns to
useFiltersTanstack(...) - render
Filterswith the returned filter state - render
DataTablewith the returned TanStack table state
Derived Select Options
When strategy="client" and a select or multiselect field does not provide options, useFiltersTanstack(...) derives them from data.
const filterDefs = defineFilters([
{
key: "status",
label: "Status",
type: "select",
},
{
key: "department",
label: "Department",
type: "select",
},
] as const);
const { filterFields } = useFiltersTanstack({
definition: filterDefs,
columns,
data,
});Static options still win when you provide them explicitly.
If you need custom labels, icons, or sorting, use optionResolvers:
import {
deriveFilterOptions,
useFiltersTanstack,
} from "@tilt-legal/cubitt-components/filters";
const tableFilters = useFiltersTanstack({
definition: filterDefs,
columns,
data,
optionResolvers: {
assignee: ({ data, getFieldValue }) =>
deriveFilterOptions({
data,
getValue: getFieldValue,
mapOption: (user) => ({
value: user.id,
label: user.name,
icon: <Avatar>{user.initials}</Avatar>,
}),
sort: (a, b) => a.label.localeCompare(b.label),
}),
},
});Mapping Filter Fields To Columns
Use fieldColumnMap when the filter key is not the same as the TanStack column id.
const tableFilters = useFiltersTanstack({
definition: filterDefs,
columns,
data,
fieldColumnMap: {
search: "name",
},
});This is useful for synthetic fields like a single search filter that targets one column id in TanStack Table.
Searching Across Multiple Values
Use searchResolvers when one filter field should inspect multiple row values.
const tableFilters = useFiltersTanstack({
definition: filterDefs,
columns,
data,
searchResolvers: {
search: (row) => [row.name, row.email, row.department],
},
});This keeps broad text search inside the same structured filter model instead of wiring a separate global filter.
Client And Server Flows
Client-Side
Use strategy="client" with data when filtering should happen in the browser.
const tableFilters = useFiltersTanstack({
definition: filterDefs,
columns,
data,
strategy: "client",
});Pass the returned columnFilters, columns, and onColumnFiltersChange into DataTable with enableFiltering.
Server-Side
Use Filters for the UI, but send filters to your backend and render the returned rows.
const tableFilters = useFiltersTanstack({
definition: filterDefs,
columns,
strategy: "server",
});
useEffect(() => {
fetchRows(tableFilters.filters).then(setRows);
}, [tableFilters.filters]);In that mode, Filters still owns the structured filter state, but your server decides what rows come back.
Legacy API
useDataTableFilters and DataTableFilter are deprecated. They still exist for legacy consumers, but the recommended path is the composed Filters plus useFiltersTanstack(...) pattern shown on this page.
API Reference
useFiltersTanstack(...) Options
| Option | Type | Default | Description |
|---|---|---|---|
definition | FilterDefinitionResult | required | Typed filter definition created with defineFilters(...) |
columns | ColumnDef<TData>[] | required | Base TanStack columns |
data | readonly TData[] | - | Client-side rows for local filtering and option derivation |
strategy | "client" | "server" | "client" | Whether filtering happens locally or externally |
defaultFilters | Filter[] | [] | Initial uncontrolled filters |
filters | Filter[] | - | Controlled filters |
onFiltersChange | (filters) => void | - | Controlled filters change handler |
fieldColumnMap | Record<string, string> | - | Map filter keys to TanStack column ids |
mapFieldToColumnId | (fieldKey) => string | undefined | - | Dynamic column-id resolver |
searchResolvers | Record<string, (row) => unknown | unknown[]> | - | Search one filter across multiple row values |
fieldAccessors | Record<string, (row) => unknown | unknown[]> | - | Override value access for filter evaluation |
filterOverrides | Record<string, FilterOverrideFn<TData>> | - | Override filter evaluation for specific fields |
optionResolvers | Record<string, FilterOptionResolver<TData>> | - | Derive custom option lists from runtime data |
overrideExistingFilterFn | boolean | false | Replace existing TanStack column filterFns |
Return Value
| Field | Type | Description |
|---|---|---|
filters | Filter[] | Current structured filter state |
setFilters | Dispatch<SetStateAction<Filter[]>> | Imperative filter state setter |
handleFiltersChange | (filters: Filter[]) => void | Change handler for Filters |
filterFields | FilterFieldsConfig | Resolved fields to pass to Filters |
columns | ColumnDef<TData>[] | TanStack columns with filter adapters applied |
columnFilters | ColumnFiltersState | Derived TanStack column-filter state |
onColumnFiltersChange | OnChangeFn<ColumnFiltersState> | Adapter back from TanStack updates |
filteredRows | TData[] | undefined | Client-side filtered rows when strategy="client" |
filteredRowCount | number | undefined | Count for the filtered client-side result |