Skip to main content

Filtering

Column filters provide type-aware text, number, date, boolean, and set-filter interfaces. Quick filter searches across visible columns.

Loading interactive example…
React
const columns: ReactColDef[] = [
{ key: "customer", label: "Customer", filter: "text" },
{ key: "status", label: "Status", filter: "set" },
{ key: "revenue", label: "Revenue", filter: "number" },
];

<Grid
rowData={rows}
columnDefs={columns}
toolbar={{ quickFilter: true }}
quickFilter={{ matchMode: "multiTerm" }}
onFilterChanged={(ev) => recomputeSummary(ev.source, ev.changedColIds)}
/>

Column filters

Set filter: true to infer the UI from ColumnType, or choose "text", "number", "date", "boolean", "set", or "tree". filterParams controls operators, debounce, buttons, case handling, and the number of AND/OR conditions.

const columnDefs: ColDef[] = [
{ key: "name", label: "Name", filter: "text" },
{ key: "amount", label: "Amount", type: ColumnType.NUMBER, filter: "number" },
{ key: "due", label: "Due", type: ColumnType.DATE, filter: "date" },
{ key: "active", label: "Active", type: ColumnType.BOOLEAN, filter: "boolean" },
{ key: "region", label: "Region", filter: "set" },
];

Active filters show an indicator in the header and are managed from the column's filter panel.

const column = {
key: "name",
label: "Name",
filter: "text",
filterParams: {
buttons: ["apply", "clear", "cancel"],
closeOnApply: true,
debounceMs: 200,
maxNumConditions: 2,
caseSensitive: false,
trimValues: true,
},
} satisfies ColDef;

When more than one condition is enabled, the filter UI lets the user join them with AND or OR. trimValues trims only filter operands, not cell values; closeOnApply needs an Apply button to have any effect.

Restrict which operators a column offers:

const column = {
key: "amount",
label: "Amount",
filter: "number",
filterParams: {
filterOptions: [
{ value: FilterType.GTE, label: "At least" },
{ value: FilterType.LTE, label: "At most" },
{ value: FilterType.IN_RANGE, label: "Between" },
],
},
} satisfies ColDef;

The built-in operators also include contains, starts/ends with, equality, inequality, blank checks, inclusion, and negated forms.

Set-filter values

Set-filter values can come from loaded rows, a static list, or an asynchronous loader:

const staticSet = {
key: "status",
label: "Status",
filter: "set",
filterParams: {
filterValues: [{ value: "Open" }, { value: "Pending" }, { value: "Closed" }],
},
} satisfies ColDef;

const asyncSet = {
key: "owner",
label: "Owner",
filter: "set",
filterParams: {
filterValues: async ({ success }) => {
success(await (await fetch("/api/owners")).json());
},
},
} satisfies ColDef;

Set filters can keep complex raw values while defining their identity and display separately:

const regionSet = {
key: "region",
label: "Region",
filter: "set",
filterParams: {
keyCreator: value => value.code,
valueFormatter: ({ value }) => value.name,
},
} satisfies ColDef;

keyCreator is used for option deduplication, selection, API lookup, and client-side comparison; valueFormatter controls labels, mini-filter matching, accessible names, and value ordering. Filter models and Set Filter API methods continue to use the raw values. Set filterParams.showValueCounts: true to show the number of loaded leaf rows for each value. Counts cover the complete client-side row model; with the server-side row model they are necessarily limited to rows currently loaded in the grid.

Set-filter label content is independently customizable while the grid retains ownership of the checkbox and interaction behavior:

const regionColumn: ReactColDef = {
key: "region",
label: "Region",
filter: "set",
filterParams: {
showValueCounts: true,
valueComponent: RegionValue,
valueComponentParams: { showFlag: true },
selectAllComponent: SelectAllRegions,
blanksComponent: UnassignedRegion,
},
};

Omitting a component uses the built-in text. Once configured, a component that renders no content leaves that label slot intentionally empty. Mini-filtering and accessible checkbox names continue to use the underlying text labels. A regular value component receives the loaded-row count as count; the Blanks component receives it too when that option exists.

Blanks are the grid's row, and you can widen it. null, undefined, and "" are blanks — never 0 or false — decided from the raw value and folded into (Blanks), so keyCreator and the filter's valueFormatter are never called with one. Write them for real values: value => value.code needs no guard.

To count something else as blank, return "" from keyCreator. That value joins (Blanks), skips the label callback too, and is stored as null in the filter model — which is what makes a valueGetter-derived value expressible, since only the application knows which of its shapes are empty.

The column's valueFormatter still sees blanks when rendering cells, which is how a blank cell shows an em dash. On the server-side row model the bucket travels as null, so the server applies its own blank rule and cannot see keyCreator.

Custom matcher

A function supplied as filter becomes the row matcher while the standard filter panel still owns its model and inputs:

const column = {
key: "tags",
label: "Tags",
filter: (value, _node, filterValues) => {
const query = String(filterValues[0] ?? "").toLowerCase();
return value.some((tag: string) => tag.toLowerCase().includes(query));
},
} satisfies ColDef;

For reusable normalization and matching, use filterParams.textFormatter and filterParams.filterFunction:

const column = {
key: "code",
label: "Code",
filter: "text",
filterParams: {
caseSensitive: false,
trimValues: true,
textFormatter: value => String(value).normalize("NFD").replace(/[\u0300-\u036f]/g, ""),
filterFunction: (type, values, cell, caseSensitive, trimValues) => {
const query = trimValues ? values[0].trim() : values[0];
const [haystack, needle] = caseSensitive
? [cell, query]
: [cell.toLowerCase(), query.toLowerCase()];
return type === FilterType.STARTS_WITH && haystack.startsWith(needle);
},
},
} satisfies ColDef;

The formatter processes both cell and filter values before comparison. A filterFunction takes precedence over a function assigned to filter, while filter: false disables filtering completely. These callbacks execute only in the client-side row model; server-side filtering must implement the equivalent normalization on the server.

Programmatic filters

getFilterModel() / setFilterModel(filters) / addFilterModel(filter) / removeFilterModel(colId) read and write all column filters as serializable { colId, filters, join } items addressed by the public ColDef.colId. The dispatch form addresses runtime columns directly:

const amount = api.getColumnModel().getByColId("amount")!;

api.dispatch({
type: "filterModelSet",
filterModel: [{
col: amount,
key: "amount",
filters: [{ type: FilterType.GTE, values: [100] }],
}],
});

Set filter by intent

For set-filter columns, prefer the intent-level helpers over manipulating in/notIn defs directly — the set filter manages its own storage:

await api.getSetFilterValues("region"); // the value universe (null = blanks)
await api.uncheckSetFilterValue("region", "EMEA"); // hide EMEA rows
await api.checkSetFilterValue("region", "EMEA"); // show them again
await api.getSetFilterState("region"); // { mode, checked, unchecked } or null
await api.setSetFilterValues("region", ["EMEA"], { mode: "include" });

Inputs are resolved against the column's value universe, so a string "5" finds the numeric 5 your rows hold, and null addresses the "(Blanks)" bucket. The methods are async because filter values may come from an async source; with the default from-rows universe they resolve immediately.

The one semantic worth choosing deliberately is mode — what happens to values that arrive after filtering (new rows, edited cells):

  • "exclude" (the menu's default behavior): the unchecked values are stored; anything else, including later arrivals, stays visible.
  • "include": the checked values are stored; anything else, including later arrivals, is hidden — "show only what I checked".

An explicit mode passed to setSetFilterValues is pinned on the filter: subsequent menu or helper toggles keep that representation instead of optimizing storage to the shorter list.

Quick filter

const options = {
quickFilter: {
mode: "always",
matchMode: "multiTerm",
caseSensitive: false,
debounceMs: 100,
showOptions: true,
},
} satisfies GridOptions;

api.setQuickFilter("open europe");
api.setQuickFilter("exact phrase", { matchMode: "substring" });

multiTerm requires every whitespace-separated term to match somewhere in the row. substring treats the input as one phrase. The toolbar can host quick filter, or mode: "onDemand" can open its floating UI with Ctrl/Cmd+F.

Quick filtering is client-side. Server-side applications receive structured column filters in each data-source request.

Reacting to filter changes

Subscribe to the canonical filterChanged event (or pass the onFilterChanged callback / bind the wrapper's filterChanged output) to recompute anything filter-derived — footer aggregates, counts, external summaries. It fires once per effective filter change, whatever the path:

  • source: "filter" — a column-filter model change (menu, API, dispatch), with the affected public changedColIds;
  • source: "quickFilter" — a quick-filter change (changedColIds is empty; client-side row model only, since the quick filter is a no-op on the server-side model);
  • source: "columns" — a columnDefs update dropped an active filter.

On the client-side row model the handler runs after the view is re-derived, so row counts and getPaginationInfo() already reflect the new filter.

By default a filter change keeps the user's current page, clamping to the last page when the result shrinks past it; list "filter" / "quickFilter" in resetPageOn to jump back to page 1 instead.