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