

## Overview [#overview]

DataTable filtering is composed from normal Cubitt surfaces:

* `defineFilters(...)` describes the filter fields
* [`Filters`](/composites/filters) renders the UI
* `useFiltersTanstack(...)` adapts those filters to TanStack Table
* `DataTable` receives the adapted `columns`, `columnFilters`, and change handlers

This is the same pattern as [Search](./search) and [Column Visibility](./column-visibility): the table stays focused on rendering, while surrounding state and UI stay composable.

<Tabs className="bg-transparent border-none rounded-none" items="[&#x22;Preview&#x22;, &#x22;Code&#x22;]">
  <Tab value="Preview" className="p-0">
    <Preview name="FiltersDataTableIntegrationExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    "use client";

    import {
      DataTable,
      defineFilters,
      Filters,
      useFiltersTanstack,
    } from "@tilt-legal/cubitt-components/composites";
    import { Badge } from "@tilt-legal/cubitt-components/primitives";
    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>
      );
    }
    ```
  </Tab>
</Tabs>

## How The Composition Works [#how-the-composition-works]

`useFiltersTanstack(...)` gives you the pieces that bridge `Filters` and `DataTable`:

* `filters` and `handleFiltersChange` for the `Filters` component
* `filterFields` which may differ from the original definition after runtime option resolution
* `columns`, `columnFilters`, and `onColumnFiltersChange` for `DataTable`

That means the recommended flow is:

1. define fields with `defineFilters(...)`
2. pass the definition plus your table columns to `useFiltersTanstack(...)`
3. render `Filters` with the returned filter state
4. render `DataTable` with the returned TanStack table state

## Derived Select Options [#derived-select-options]

When `strategy="client"` and a `select` or `multiselect` field does not provide `options`, `useFiltersTanstack(...)` derives them from `data`.

```tsx
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`:

```tsx
import {
  deriveFilterOptions,
  useFiltersTanstack,
} from "@tilt-legal/cubitt-components/composites";

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 [#mapping-filter-fields-to-columns]

Use `fieldColumnMap` when the filter key is not the same as the TanStack column id.

```tsx
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 [#searching-across-multiple-values]

Use `searchResolvers` when one filter field should inspect multiple row values.

```tsx
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-and-server-flows]

### Client-Side [#client-side]

Use `strategy="client"` with `data` when filtering should happen in the browser.

```tsx
const tableFilters = useFiltersTanstack({
  definition: filterDefs,
  columns,
  data,
  strategy: "client",
});
```

Pass the returned `columnFilters`, `columns`, and `onColumnFiltersChange` into `DataTable` with `enableFiltering`.

### Server-Side [#server-side]

Use `Filters` for the UI, but send `filters` to your backend and render the returned rows.

```tsx
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 [#legacy-api]

<Callout type="warning">
  `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.
</Callout>

## API Reference [#api-reference]

### `useFiltersTanstack(...)` Options [#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 `filterFn`s               |

### Return Value [#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          |
