Data Filter
A composable filter builder over a consumer-owned useReactTable() instance — a single "+ Filter" trigger chains column → operator → value into a clickable, removable chip.
| Title | Status | Priority | Done |
|---|---|---|---|
| Design onboarding flow | In Progress | 2 | No |
| Fix invoice rounding bug | Done | 1 | Yes |
| Write Q3 report | Todo | 3 | No |
| Review PR #482 | In Progress | 1 | No |
DataFilter is a headless-composition component over the same
useReactTable() instance you'd pass to Data Grid
— they aren't coupled, and a consumer can use either alone. They share only
the table instance and the column.meta type contract from
Data Grid Types. All eight column types
have a working operator set here — text, number, currency, date,
select, boolean, multiSelect, and user. user shares select's
operators verbatim, with "Unassigned" as a pickable null value rather than a
dedicated operator.
Installation
pnpm dlx shadcn@latest add @syncblocks/data-filterUsage
import { DataFilter, dataGridFilterFn } from "@/components/ui/data-filter"const columns: ColumnDef<Task>[] = [
{ accessorKey: "title", header: "Title", meta: { type: "text" }, filterFn: dataGridFilterFn },
// ...
]
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
})
<DataFilter table={table} />Every filterable column needs both a meta.type and
filterFn: dataGridFilterFn — the type drives the operator set and value
input this component renders, and dataGridFilterFn is what actually
evaluates the { operator, value } object the chip chain writes via
column.setFilterValue(). None of TanStack's own built-in filter functions
can read that shape; they all expect the filter value directly. The table
also needs getFilteredRowModel() — DataFilter only writes filter state,
it never filters rows itself.
Features
- A single "+ Filter" trigger rather than a control per column, so the
toolbar stays a constant size no matter how wide the table is. Filterable
columns derive from
meta.type(one of the six types above) pluscolumn.getCanFilter()— excluding a column from the builder is an ordinary TanStack concern (enableColumnFilter: false), not a second config list. - Column → operator → value chain — the popover walks through picking a
column, then an operator (from that column's type-specific set), then a
value control shaped for the operator: a single input for most, two
inputs for "is between", a searchable list for
selectis/is-not, and a searchable multi-select for "is any of".booleanends at the operator — "is true"/"is false" commit immediately, with no value step at all. - Each committed filter renders as a
[ column | operator | value | x ]pill, not one opaque chip. The operator segment is its own dropdown of that column's operators — pick a new one and it commits immediately, keeping the existing value when the new operator has the same shape (>→<) and resetting it when it doesn't (=→is between). The value segment independently reopens the same value editor the create-new chain uses, prefilled with the current value, so adjusting a threshold is one click on just that segment rather than deleting and rebuilding the whole condition. An inline "x" on each pill removes it. dataGridFilterFn— the exportedfilterFn. It unwraps{ operator, value }and dispatches oncolumn.meta.type, and setsautoRemoveso clearing a chip removes the filter through TanStack's owncolumnFilterspath rather than a parallel one.- A column already filtered drops out of the column-pick list — its chip is the edit path, so the "+ Filter" trigger only ever offers columns that don't already have a condition.
- Narrow-container toolbar —
DataFiltermeasures its own container with aResizeObserver(never the viewport; this is independent ofDataGrid's own container measurement). Below 448px the chip chain and the "+ Filter" popover give way to two swipeable bottom-drawer toolbar buttons, so chrome height stays constant no matter how many filters are active:- "Sort" is labelled with the current sort column and direction. Tapping it opens a bottom drawer listing every sortable column; tapping one cycles asc → desc → unsorted and closes the drawer. Sorting on this branch is single-column — picking a different column replaces the whole sort array. A pre-existing multi-column sort (set while wide) shows as the primary column plus a muted "+N more" and is left untouched until a new pick replaces it.
- "Filter" carries a count badge and opens a bottom drawer listing
active filters as full-width rows with a persistent "Add filter" row.
Tapping "Add filter" or an existing row pushes the same
column → operator → value chain as a second step inside the drawer
(skipping the column step when editing an existing row) — the same
{ operator, value }write throughcolumn.setFilterValue()as the wide-branch popover. - Active filters stay visible as chips below the toolbar row, not just
the "Filter" badge count — mirroring the wide branch's always-visible
pills instead of hiding them inside the drawer. Each chip is a
ButtonGroupof twoButtons:formatFilterSummary's "column operator value" text (size="xs") plus an icon-only "×" (size="icon-xs"). Tapping the label button opens the drawer straight to that filter's operator step (skipping the list), and the "×" removes it directly without opening the drawer at all. Unlike the wide-branch pill, a narrow chip has no separate operator/value tap targets — one tap always opens the full edit chain, since three independent hit targets don't fit at this width.
Composable parts
DataFilter bundles sort + filter into one bar. When a host toolbar wants to
place the pieces itself (as DataGrid does),
two of them are exported separately, both taking the same table:
<DataFilterPopover table trigger />— the column → operator → value chain behind a trigger element you supply.<DataFilterChips table className? />— the active-filter chip row with a trailing "Clear filters". Renders nothing when no filter is set, so it can be dropped in unconditionally.<DataFilterChain table open? editColumnId? />— the narrow-branch filter list and chain with no drawer around it, for a host sheet that wants it as one section among others. Flippingopento true resets it to the list step;editColumnIdseeds it straight to that filter's operator step.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
table | Table<TData> | - | A useReactTable() instance you create and own — the same one you'd pass to DataGrid. Filterable columns need meta.type and filterFn: dataGridFilterFn; the table needs getFilteredRowModel(). (required) |