React table header filters — per-column funnels
▶ See it working: Filtering in Mantine — switch the layout to Header: each filtered column gets a funnel, and Filters keeps the AND/OR tree. The same page exists for every kit.
Header filters put each filter where its column is. Compose headerFilters()
from @adapttable/<kit>/header-filters and every column with a filter
definition shows a funnel button in its header cell. The funnel opens the same
kit-native field the Filters panel draws, bound to the same state, URL params
and chips. The filter definitions still come from filters(…)
(filtering); header filters only move where the fields open.
Example
Section titled “Example”import { DataTable } from "@adapttable/mantine"; // or mui, chakra, antd, radix, base-ui, shadcn, unstyledimport { filters } from "@adapttable/mantine/filters";import { headerFilters } from "@adapttable/mantine/header-filters";
interface Project { id: string; name: string; owner: { name: string }; status: "active" | "paused" | "done"; budget: number; dueAt: string; // ISO date}
const projects: Project[] = [ { id: "1", name: "Billing rewrite", owner: { name: "Amira Haddad" }, status: "active", budget: 42000, dueAt: "2026-11-30", }, { id: "2", name: "Search index", owner: { name: "Jonas Weber" }, status: "paused", budget: 18000, dueAt: "2026-10-15", }, { id: "3", name: "Design tokens", owner: { name: "Priya Nair" }, status: "done", budget: 9500, dueAt: "2026-08-01", }, { id: "4", name: "Audit log", owner: { name: "Amira Haddad" }, status: "active", budget: 27000, dueAt: "2027-01-20", },];
export function Projects() { return ( <DataTable data={projects} rowKey={(row) => row.id} columns={[ { key: "name", filter: "text", sortable: true }, { key: "owner", header: "Owner", accessor: (row) => row.owner.name }, { key: "status", filter: { type: "select", options: [ { value: "active", label: "Active" }, { value: "paused", label: "Paused" }, { value: "done", label: "Done" }, ], }, }, { key: "budget", filter: "numberRange", sortable: true }, { key: "dueAt", header: "Due", filter: "dateRange" }, ]} features={[ filters([ // The state key is a row path; `column` puts the funnel on "owner". { key: "owner.name", column: "owner", type: "text", label: "Owner" }, ]), headerFilters(), ]} closeHeaderFilterOnSelect /> );}Both features are required. filters([…]) turns the column filter
declarations and the standalone definitions into a filter runtime;
headerFilters() draws a funnel for each of them.
How it works
Section titled “How it works”- Composing it selects header mode. The table resolves one filter
container through
resolveFilterMode(filtersMode, headerFilters)in@adapttable/core: header mode wins wheneverheaderFilters()is composed, whateverfiltersModesays, so the toolbar popover or drawer and the header funnels are never mounted for the same fields.filtersMode="header"without the feature resolves to header mode too, but nothing draws the funnels — composeheaderFilters(). - One funnel per column. A column shows a funnel when a definition
matches it:
filterDefForColumnpicks the first definition whosecolumn ?? keyequals the column key. A columnfilterdeclaration matches its own column. A standalone definition whose state key differs from the column key setsFilterDef.column—key: "owner.name"undercolumn: "owner"above. A definition that matches no column gets no funnel. - The same field as the panel. The funnel’s overlay renders the kit’s
auto-built form for that single definition: the operator-first text,
number and date widgets, selects, multi-selects, the checklist, and any
custom type’s
render(custom filter types). Writes go to the same extra bag, so the URL, chips, the page reset and (on frontend data) the row predicate behave exactly as they do from the panel. - Active state. A funnel whose definition has any non-empty state key
carries
data-activeand the kit’s active styling. - Staying open. The overlay stays open while a field is being filled — picking an operator, typing a term, toggling multi-select options. A dropdown the kit portals out of the overlay (a select menu, a date picker) is not treated as an outside click. A true outside press or Escape closes it.
closeHeaderFilterOnSelect. Off by default. When set, the overlay also closes after a finished single-control write: aselectorbooleanvalue, or a valueless operator such as “Is empty”. Typed text, range bounds and multi-value lists never close it, because another control is still waiting. The check reads the type’swidget, so a custom type drawn as aselectcloses the same way.
The Filters button and the AND/OR tree
Section titled “The Filters button and the AND/OR tree”In header mode the per-field list leaves the Filters panel — the funnels own
those fields. The toolbar keeps its Filters button only for the
AND/OR filter tree, which has no column of its own
(toolbarShowsFilters). Every built-in source exposes
source.setFilterTree, so the button stays and opens the tree builder
expanded.
Options
Section titled “Options”| Option / prop | Type | Default | Description |
|---|---|---|---|
headerFilters() |
factory, no arguments | off | From @adapttable/<kit>/header-filters. A funnel per filtered column; selects header mode. Needs filters(…). |
closeHeaderFilterOnSelect |
boolean (DataTable prop) |
false |
Close the overlay after a finished single-control write. Outside press and Escape always close it. |
filtersMode |
"popover" | "drawer" | "header" |
"popover" |
Ignored for the fields while headerFilters() is composed; header mode wins. |
FilterDef.column |
string |
key |
Column whose header carries this definition’s funnel. |
labels.headerFilters |
string |
"Column filters" |
Accessible name of the exported compact filter row (FilterHeaderRow). |
Per-kit differences
Section titled “Per-kit differences”Every kit draws the funnel with its own button and popover; the field inside is that kit’s auto-built filter form.
- Ant Design renders its own table header, so the funnel lives inside the
column
title. That keepsfixedcolumns on antd’s own header. The overlay is portalled todocument.bodyand positioned against the funnel, following scroll and resize. The funnel stops its click from reaching the header, so opening it never toggles antd’s sort. Open state is held above the header (HeaderFilterOpenProvider), because antd rebuilds column titles on every filter write. - The other kits render the funnel inside the header cell next to the caption and the sort control, so it moves with pinned columns, sticky headers and column windowing.
For custom header markup, each kit also exports FilterHeaderControl (one
column’s compact control) and FilterHeaderRow (a filter row for a custom
header) over FilterHeaderControlChrome / FilterHeaderChrome. The overlay
logic is headless in @adapttable/react: useHeaderFilterOverlay,
bindHeaderFilterDismiss, headerFilterFieldIsComplete and
usePointerDismiss.
- Desktop only. Mobile cards have no column headers, so no funnel
renders below the mobile breakpoint. Active filters still show as removable
chips, and the Filters button still opens the AND/OR tree. A table that
must offer per-field filters on phones uses
filtersMode="popover"or"drawer"withoutheaderFilters(). - URL state and saved views. Header filters write the same
f_<key>params as the panel —f_nameandf_nameOp,f_budgetMin/f_budgetMax,f_dueAtFrom/f_dueAtTo,f_owner.nameabove — so a shared link or a saved view reopens with the same funnels active. See URL state. - Keyboard and screen readers. The funnel is a real button, reached with
Tab and named by the definition’s label (
filterLabel: thelabel, or the humanized key). Escape closes the overlay. - The standard preset.
standardFeatures()includesheaderFilters(), so a table composed from the preset opens its filters from the headers. The preset composesfilters(…)only when itsfiltersoption is given — passstandardFeatures({ filters: [] })when every filter is declared on a column. Compose the members individually for popover or drawer filters — see feature composition.