---
id: "packages/active-grid/filters"
title: "Filtering"
description: "Filter types, operators, presets, date filters with time zones, and the global search."
url: "/packages/active-grid/filters"
locale: "en"
revision: "4b7e481cd0c64cb3"
---

# Filtering

> [!NOTE]
> **The package's own guide**
>
> This guide is the package's `docs/guides/filters.md`, included unchanged from
> the package as it is in Foundation. Links to the package's internal design
> documents keep their text.

How to configure column filters in Active Grid: filter types, operators,
presets, date filters (including time zones and calendar locales) and the
global search. Type signatures, operator factories and filter components are in
the API reference: `../api/filters.md`.

## Filter Types

Active Grid provides the following standard filter widgets, configured via `meta.filterType` on
column definitions:

| Type | Component | Operators | Use Case |
| --- | --- | --- | --- |
| `text` | `TextFilter` | 8 operators | Names, descriptions, codes |
| `number` | `NumberFilter` | 7 default operators | Amounts, quantities, percentages |
| `date` | `DateFilter` | 4 default operators | Dates and timestamps |
| `select` | `SelectFilter` | Multi-select | Enum values, categories, statuses |
| `boolean` | `BooleanFilter` | 1 default operator | Yes/no flags |

**`filterType` chooses the widget; the capability decides whether there is one
at all.** Both the filter row and each filter cell require
`column.getCanFilter()` as well. In client mode that is true by default, so the
table above reads as it always did. In server mode a column without a
`server.filter` mapping (or without an entry in the `serverCapabilities` filter
map) renders no control, whatever its `filterType` says — including a
`filterType` injected by a column type preset. Presentation may narrow the
server contract, never widen it. See
`../api/columns.md` § Filtering.

An additional `interval` type accepts numeric `from`/`to` bounds and a
`within`/`overlap` mode through `IntervalFilterValue`; its host supplies the
interval input UI. The tables below include optional absence operators, which
are not in the default number/date/boolean lists (see `withEmpty`).

## Text Operators

| Operator | Label | Requires Value | Description |
| --- | --- | --- | --- |
| `contains` | Contains | Yes | Case-insensitive substring match |
| `notContains` | Does not contain | Yes | Inverse of contains |
| `equals` | Equals | Yes | Exact match (case-insensitive) |
| `notEquals` | Does not equal | Yes | Inverse of equals |
| `startsWith` | Starts with | Yes | Prefix match |
| `endsWith` | Ends with | Yes | Suffix match |
| `isEmpty` | Is empty | No | `null`, `undefined`, or `''` |
| `isNotEmpty` | Is not empty | No | Inverse of isEmpty |

## Number Operators

| Operator | Label | Requires Value | Description |
| --- | --- | --- | --- |
| `equals` | Equals | Yes | Exact numeric match |
| `notEquals` | Does not equal | Yes | Inverse of equals |
| `greaterThan` | Greater than | Yes | Strictly greater |
| `greaterThanOrEqual` | Greater than or equal | Yes | Greater or equal |
| `lessThan` | Less than | Yes | Strictly less |
| `lessThanOrEqual` | Less than or equal | Yes | Less or equal |
| `between` | Between | Yes (two values) | Inclusive range: `value >= from && value <= to` |
| `isEmpty` | Is empty | No | `null` or `undefined` |
| `isNotEmpty` | Is not empty | No | Inverse of isEmpty |

## Date Operators

| Operator | Label | Requires Value | Description |
| --- | --- | --- | --- |
| `onDate` | On date | Yes | Exact date match (day precision) |
| `before` | Before | Yes | Strictly before the given date |
| `after` | After | Yes | Strictly after the given date |
| `inRange` | In range | Yes (two values) | Between start and end date (inclusive) |
| `isEmpty` | Is empty | No | `null` or `undefined` |
| `isNotEmpty` | Is not empty | No | Inverse of isEmpty |

## Boolean Operators

| Operator | Label | Requires Value | Description |
| --- | --- | --- | --- |
| `equals` | Is | Yes | Match `true` or `false` |
| `isEmpty` | Is empty | No | `null` or `undefined` |
| `isNotEmpty` | Is not empty | No | Inverse of isEmpty |

## Select Filter

The select filter uses a multi-select dropdown rather than operators. Users
select one or more values from a list.

Filter value is an array of selected values: `['active', 'pending']`.

Options come from (in priority order):

1. `meta.filterOptions` — Explicit options array.
2. Server-side `facets` — Returned by `fetchFn` in server mode.
3. Client-side faceted values — Auto-computed from column data.

Configuration:

```typescript
{
  meta: {
    filterType: 'select',
    filterOptions: [
      {label: 'Active', value: 'active'},
      {label: 'Inactive', value: 'inactive'},
    ],
    filterSearchable: true,      // Enable search within options
    filterShowSelectAll: true,    // Show "Select all" toggle
  },
}
```

## FilterValue Type

Filter values are discriminated unions based on the operator:

```typescript
// No-value operators (isEmpty, isNotEmpty)
{operator: 'isEmpty'}

// Single-value operators
{operator: 'contains', value: 'john'}
{operator: 'greaterThan', value: 50000}
{operator: 'onDate', value: '2025-06-15'}
{operator: 'equals', value: true}

// Range operators (between, inRange)
{operator: 'between', value: 1000, valueTo: 5000}
{operator: 'inRange', value: '2025-01-01', valueTo: '2025-12-31'}
```

TypeScript enforces the shape through a discriminated union; the type
definitions are in `../api/filters.md` § FilterValue Types.

## Operator Presets

A preset restricts the operators a column offers. Select one by name with
`meta.operatorPreset`:

```typescript
{meta: {filterType: 'text', operatorPreset: 'identity'}}
```

### What a preset can and cannot do

**A preset only ever NARROWS.** It is applied as a filter over the operators the
column already offers — the per-type defaults on a client-mode grid, or the
server-declared capability list on a resource-backed one. It can therefore never
add an operator the server does not allow, and an unknown or unusable preset can
never widen the offer either. This is the property that makes a custom preset
safe to accept from configuration.

The consequence is worth stating plainly: a preset that names an operator the
column does not have available is a no-op for that operator, not an opt-in. See
[`withEmpty`](#withempty) for the case where that matters.

### Built-in presets

| Name | Narrows to | Use for |
| --- | --- | --- |
| `identity` | `equals`, `notEquals` | IDs, codes, enum values. |
| `searchOnly` | `contains` | A column that behaves like a search bar. |
| `rangeOnly` | `greaterThan`, `lessThan`, `between`, `greaterThanOrEqual`, `lessThanOrEqual` | Statistics, years, measurements. |
| `comparison` | `equals` plus the four comparisons | Numeric filters where exact match is also relevant. |
| `noEmpty` | everything except `isEmpty` / `isNotEmpty` | Columns where "no value" is not a meaningful question. |
| `withEmpty` | comparisons plus `isEmpty` / `isNotEmpty` | Nullable number/date columns — see the note below. |

#### `withEmpty`

`isEmpty` / `isNotEmpty` are deliberately absent from the default operator set
for number, date and boolean columns (the rationale is on `getTextOperators` in
`types/filters.ts`). `withEmpty` is the opt-in for the rare nullable column
where the question does make sense.

**It only takes effect where the operator is already available**, i.e. on a
resource-backed grid whose capability declares `isEmpty` for that column. On a
plain client-mode grid the base list does not contain `isEmpty`, and a preset
narrows rather than widens, so the preset changes nothing there. The same limit
applies to `meta.allowedOperators`.

Use it only on columns whose underlying field is actually nullable (`String?`,
`Int?`, `DateTime?`). On a non-nullable field the query builder in
`@workspace/active-resources` does not build a null branch: a text column then
matches the empty string only, and for number, date and boolean the operator
allowlist rejects `isEmpty` before a condition is built at all. Declaring
`nullable: true` on a field that is not nullable is what `validateRegistry`
refuses.

### Declaring your own presets

`<GridDefaults>` may override a built-in preset and declare additional ones.
The map is merged **per key** with the built-ins, so adding one preset never
removes the others:

```tsx
<GridDefaults
  filtering={{
    operatorPresets: {
      // Override a built-in: every column that says `identity` follows this.
      identity: ['equals'],
      // Add one of your own.
      codeLookup: ['equals', 'startsWith'],
    },
  }}
>
  <App />
</GridDefaults>
```

```typescript
{meta: {filterType: 'text', operatorPreset: 'codeLookup'}}
```

The built-in names keep autocomplete on `meta.operatorPreset`; an additional
name is an ordinary string. The union of built-in names is exported as
`BuiltInOperatorPreset` for consumers writing shared column factories. Widening
that one field does not loosen the rest of the column meta — `filterType` and
`allowedOperators` stay closed unions.

There is no way to *remove* a built-in preset. Removing one would break the
columns that name it.

### How a bad preset behaves

Every case is explicit, dev-warned, and fails towards "no narrowing":

| Situation | Result |
| --- | --- |
| The name is not a known preset | The column keeps its full operator list. A preset can only narrow, so an unknown name can never widen the offer. |
| An entry is not a filter operator (a typo) | That entry is dropped; the rest of the preset applies. |
| The same operator appears twice | Deduplicated, keeping the first position. |
| The preset is empty, or every entry was bad | Refused. An empty narrowing would leave the filter with nothing to choose. |
| The preset names an operator the server forbids | Intersected away. Narrowing cannot widen a capability. |

Warnings appear in development only (`process.env.NODE_ENV !== 'production'`)
and are emitted once per column and reason.

## Custom Operator Restriction

For fine-grained control on a single column, use `meta.allowedOperators`:

```typescript
{
  meta: {
    filterType: 'number',
    allowedOperators: ['greaterThan', 'lessThan', 'between'],
    defaultFilterOperator: 'greaterThan',
  },
}
```

## Enabling Operator Selection

By default, filters use a single default operator. To show the operator
selector dropdown:

```typescript
{meta: {filterType: 'text', enableFilterOperators: true}}
```

Default operators per type (when `enableFilterOperators` is false):

- Text: `contains`
- Number: `equals`
- Date: `onDate`
- Boolean: `equals`

## Date Filter Configuration

### Presentation and time zone

The host supplies the reader's presentation through `locale`, including an
explicit time zone for relative dates and calendars. The grid does not resolve
application settings. Labels come from the host translator; date notation and
calendar locales delegate to `@workspace/active-i18n`.

```tsx
const presentation = {languageLocale: 'en-GB', timeZone: 'UTC'};

<ActiveGrid
  mode="client"
  gridId="measurements"
  locale={presentation}
  data={measurements}
  columns={columns}
/>;
```

Keep an object presentation stable across renders. The example chooses UTC
explicitly; a real host supplies the time zone appropriate to its data view.

### Presets

`DEFAULT_DATE_PRESETS` includes today, yesterday, last 7 days, last 30 days,
this month, last month, this quarter and this year. Their `getValue` receives
the grid presentation. The relative day comes from its explicit `timeZone`;
a missing zone throws a configuration error rather than using the browser's.

The returned `Date` carries calendar-day fields at local midnight. The date
filter reads those fields into `yyyy-MM-dd` strings. These are calendar bounds,
not UTC timestamps. Do not convert them to instants before giving them back to
the date filter.

A column may supply its own `meta.dateFilterPresets`, or `false` to hide the
preset list:

```typescript
// Disable presets entirely — calendar only
{meta: {filterType: 'date', dateFilterPresets: false}}
```

A custom `DateFilterPreset.getValue(presentation?)` returns
`{from: Date, to?: Date}` and runs when selected. A fixed calendar range can be
concrete without assuming a time zone:

```typescript
import type {DateFilterPreset} from '@workspace/active-grid';

const reviewPeriod: DateFilterPreset = {
  key: 'records.reviewPeriod',
  label: t('records.reviewPeriod'),
  type: 'range',
  getValue: () => ({
    from: new Date(2026, 0, 1),
    to: new Date(2026, 2, 31),
  }),
};
```

For a relative custom preset, obtain the calendar day through the central
active-i18n date helpers using the explicit presentation zone, never from the
browser clock (`new Date()` plus date arithmetic reads the runtime's zone). The
host owns what the range means; the grid owns its filter representation.

### Range Mode

There are three ways to enable date range filtering:

**1. Operator-driven range (recommended)** — set
`enableFilterOperators: true`. The calendar switches to range mode
automatically when the user selects the `inRange` operator. All other
operators (`onDate`, `before`, `after`) show a single date picker.

```typescript
// Operator dropdown controls the mode — user picks "In range" for range selection
{meta: {filterType: 'date', enableFilterOperators: true}}
```

**2. Forced range mode** — set `dateFilterMode: 'range'`. The calendar is
always in range mode. Use for financial reporting or period selection where
you always want a date range, without exposing the operator dropdown.

```typescript
// Always show range calendar — no operator selector needed
{meta: {filterType: 'date', dateFilterMode: 'range'}}

// Range with date bounds
{meta: {filterType: 'date', dateFilterMode: 'range',
        dateFilterMinDate: '2020-01-01', dateFilterMaxDate: '2026-12-31'}}
```

> **Note:** Do not combine `enableFilterOperators: true` with
> `dateFilterMode: 'range'` — the forced range-mode overrides the
> operator-driven switching, making the operator-dropdown's
> `inRange`/single-date distinction ineffective. Use one or the other.

**3. Toggle mode** — set `dateFilterMode: 'both'`. Shows a toggle that lets
the user switch between single-date and range mode.

### Manual Date Entry

The date filter includes a text input field above the calendar. Users type
the numeric date pattern of the grid's presentation (`getDateInputPattern`,
active-i18n): `dd-MM-yyyy` for `nl`, `dd.MM.yyyy` for `de`, `yyyy/MM/dd` for
`ja-JP`, and so on for any locale the formatters accept. Every Unicode digit
set is read (`٠٩-٠٣-٢٠٢٦` is 9 March 2026); no textual or localized date
grammar is accepted beyond that pattern.

The text input and calendar are bidirectionally synced. In range mode, two
text inputs are shown ("from" and "to").

### Calendar locale

`getDateFnsLocaleForGrid(presentation)` delegates to active-i18n's composed
locale: language determines words and presentation preferences determine
notation. Active-grid has no separate language registry, and the calendar
refuses no locale the formatters accept:

- month and weekday names come from that locale (derived from Intl for a tag
  without bundled date-fns wording), always in the Gregorian calendar;
- the accessible labels (previous and next month, today, week) come from
  Intl (`getCalendarPhrases`);
- day numbers are written in the locale's digits where the calendar library
  supports that numbering system;
- the week starts where `Intl.Locale` week info says, and on Monday when the
  runtime has none.

A tag the formatters refuse (malformed, without Intl data, or asking for a
non-Gregorian calendar) throws active-i18n's UnsupportedLocaleError.

The `calendarLocale` prop supplies a date-fns locale-object override for the
calendar. It does not replace the grid's value presentation or its explicit
time zone. If the object carries its own `labels` (a `react-day-picker/locale`
import does), those labels win over the Intl-derived ones. For a custom
calendar, derive it from the grid's effective presentation:

```tsx
import {useGridLocale} from '@workspace/active-grid';
import {getDateFnsLocale} from '@workspace/active-format';

function CustomDateInput() {
  const calendarLocale = getDateFnsLocale(useGridLocale());
  // Use calendarLocale in the custom calendar.
}
```

The built-in date filter performs this resolution itself. Date input parsing
supports the package's declared formats; a calendar library's wider range of
calendars, layouts or parsing conventions is not automatically a grid feature.
The internal owners are `src/utils/date-filter-presets.ts`,
`src/utils/date-locale.ts` and `src/components/filters/date-filter.tsx`.

## Fuzzy Search

The global search (toolbar) uses a 5-level fuzzy matching algorithm:

| Level | Strategy | Example: searching "jhn" |
| --- | --- | --- |
| 1 | Exact match | `"jhn"` matches `"jhn"` |
| 2 | Starts with | `"jhn"` matches `"jhnson"` |
| 3 | Word boundary | `"jhn"` would not match (no word starts with "jhn") |
| 4 | Contains | `"jhn"` would not match (not a substring) |
| 5 | Sequential characters | `"jhn"` matches `"j-o-h-n"` (j...h...n found in sequence) |

Fuzzy search is enabled by default. Disable via
`GridConfig.behavior.enableFuzzyFilter`:

```tsx
<GridDefaults behavior={{enableFuzzyFilter: false}}>
```

## Server-Side Filtering

In server mode, filter values are passed directly to the `fetchFn` in
`params.filters`. The grid does not evaluate filters client-side — the
server is responsible for applying them.

Column filter values arrive as `FilterValue` objects (with operators) or as
arrays (for select filters). See `server-mode.md` for
implementation details.

A date filter sends calendar strings in `value` and, for a range, `valueTo`.
The resource server validates and interprets those values according to the
server field's contract; a JavaScript `Date` from a preset is not forwarded
unchanged as a UTC timestamp. Calendar-day columns and timestamp columns have
different semantics: if a server query needs an instant interval, its server
owner derives the interval in the declared zone. Do not make the browser's zone
an implicit query owner.

## Operator Labels

Operator labels come from the host translator. The operator factories
(`getTextOperators(t)`, `getDateOperators(t)`, `getOperatorsForType(type, t)`
and the rest) take it as their argument; the static `*_OPERATORS` arrays carry
English defaults and apply only when no translator is supplied. A supplied
translator is authoritative, including any raw key it returns. The factories,
the arrays and the label keys are listed in
`../api/filters.md` § Operator Arrays + Factories.

## Boolean Filter Variants

The boolean filter supports two display modes.

### Text variant (default)

Three text buttons: All / Yes / No.

```typescript
{meta: {filterType: 'boolean', booleanFilterVariant: 'text'}}
```

Custom labels:

```typescript
{
  meta: {
    filterType: 'boolean',
    booleanFilterLabels: {
      all: 'Any',
      true: 'Enabled',
      false: 'Disabled',
    },
  },
}
```

### Icon variant

Three icon buttons: — / ✓ / ✗.

```typescript
{meta: {filterType: 'boolean', booleanFilterVariant: 'icon'}}
```
