# Filtering

> Global search, per-column filters and operators, controlled filter state, and how filtering composes with pagination and selection.

Page: https://svelte-lean.vercel.app/table/filtering · Markdown: https://svelte-lean.vercel.app/table/filtering.md · Index: https://svelte-lean.vercel.app/llms.txt

Search and column filters are two fields of the same state. Search reads the searchable, visible columns; a filter names one column, an operator and a value. Both compose with sorting, pagination and selection through the row model.

- **Source**: core.ts (matchesFilter, fold, prepareRows)table.svelte.ts
- **Tests**: core.test.tsdata-table.dom.test.tstable.test.tstable.spec.ts (browser)
- **Fixture**: fixtures/table-basic
- **Benchmark**: table.bench.spec.ts

Search splits its text on whitespace and keeps a row when every term occurs in at least one searchable column. Matching folds case and accents with the `locale`, including the Turkish dotted and dotless i, so "ipek" finds "İpek". Search never reads a field that is not a declared, visible column unless `searchText` says so: hidden columns and undeclared payload fields do not leak into results.

Column filters carry an operator. The built-in inputs write `contains`; any other operator is written by application code into `state.filters`, as the revenue input below does. A column can replace the operator matcher with its own `filter` predicate. In a tree, a parent stays visible when a descendant matches. A filter change resets the page index.

`searchText` replaces what a row is searched by: a function of the row that returns the text, for example fields that are not columns or a combination of them. Terms are folded the same way. `searchDelay` (milliseconds) debounces the search box and the filter inputs: the state changes after the pause, which matters when every state change is a server request, and clearing a field applies at once. The engine applies `state.search` whether or not the table draws its own box, so a search field in a card header or a toolbar writes the state and the table follows.

`search text, delay and an outside search box`

```svelte
<script lang="ts">
	let tableState = $state<Partial<TableState>>({});
</script>

<!-- A search box elsewhere in the page writes state.search; the table has none of its own. -->
<input type="search" aria-label="Search members" bind:value={() => tableState.search ?? '', (search) => (tableState = { ...tableState, search, pageIndex: 0 })} />

<DataTable
	{data}
	{columns}
	getRowId={(row) => row.id}
	bind:state={tableState}
	searchText={(row) => `${row.name} ${row.email} ${row.team}`}
	filters
	searchDelay={250}
/>
```

`operators`

```ts
// state.filters: one entry per column; the default operator is 'contains'.
[
	{ id: 'team', value: 'eng' },                          // contains, case and accent folded
	{ id: 'status', operator: 'equals', value: 'Active' },
	{ id: 'revenue', operator: 'gte', value: 10000 },
	{ id: 'joined', operator: 'between', value: ['2026-03-01', '2026-06-30'] },
	{ id: 'region', operator: 'in', value: ['Europe', 'Asia'] },
	{ id: 'email', operator: 'empty', value: false }       // false: only rows WITH a value
]
```

`column predicate`

```ts
// A column predicate replaces the operator matcher for that column.
{
	id: 'progress',
	header: 'Progress',
	filter: (value, filter) => Number(value) >= Number(filter.value)
}
```

## Search and column filters

Combine global search with per-column filters and custom operators.

Filtering.svelte

```svelte
<script lang="ts">
	import { DataTable, defaultState, type Column, type TableState } from '@svelte-lean/table';
	import { memberColumns } from '$lib/demo/columns';
	import { people, type Person } from '$lib/demo/data';

	const data = people(36);
	const columns: Column<Person>[] = memberColumns().map((column) =>
		column.id === 'revenue' || column.id === 'progress' || column.id === 'joined'
			? { ...column, filterable: false }
			: column
	);
	let tableState = $state<Partial<TableState>>(defaultState({ pageSize: 8 }));

	// A filter outside the table: an operator other than "contains" on a column without an input.
	function minimumRevenue(value: string) {
		const others = (tableState.filters ?? []).filter((filter) => filter.id !== 'revenue');
		tableState = {
			...tableState,
			pageIndex: 0,
			filters: value ? [...others, { id: 'revenue', operator: 'gte', value: Number(value) }] : others
		};
	}
</script>

<label>Revenue at least <input type="number" min="0" step="1000" oninput={(e) => minimumRevenue(e.currentTarget.value)} /></label>

<DataTable {data} {columns} getRowId={(row) => row.id} bind:state={tableState} searchable filters label="Members" />
```

The search input and the filter row are the built-in controls; Revenue, Progress and Joined are filterable: false and get no input. The revenue field outside the table writes a gte filter into the same state. The pre element prints search and filters as they change.

- The filtered total drives the page count and the status line under the table.
- With `selectionScope="filtered"`, select-all covers every filtered row, not only the page.
- In manual mode the engine applies no search or filter; the state describes what the server should do.

## API

| Name | Of | Type | Default | Description |
| --- | --- | --- | --- | --- |
| [`searchable`](https://svelte-lean.vercel.app/table/api.md#prop-searchable) | Controls | `boolean` | `false` | Shows the search input. state.search applies without it, so a search box elsewhere in the page can write the state. |
| [`searchText`](https://svelte-lean.vercel.app/table/api.md#prop-searchtext) | Controls | `(row: T) => string` | `undefined` | The text search terms are matched against, per row. Without it: the values of the searchable, visible columns. |
| [`searchDelay`](https://svelte-lean.vercel.app/table/api.md#prop-searchdelay) | Controls | `number` | `0` | Milliseconds between the last keystroke in the search box or a filter input and the state change; clearing a field applies at once. |
| [`filters`](https://svelte-lean.vercel.app/table/api.md#prop-filters) | Controls | `boolean` | `false` | Adds a filter input under each filterable leaf header (operator contains). |
| [`locale`](https://svelte-lean.vercel.app/table/api.md#prop-locale) | Presentation | `string` | `undefined` | Locale of the collator used for sorting and of case folding in search and filters. |
| [`search`](https://svelte-lean.vercel.app/table/api.md#state-search) | TableState | `string` | `''` | The search text; terms are split on whitespace. |
| [`filters`](https://svelte-lean.vercel.app/table/api.md#state-filters) | TableState | `Filter[]` | `[]` | Column filters with operator and value. |
| [`searchable`](https://svelte-lean.vercel.app/table/api.md#column-searchable) | Sort, filter and aggregate | `boolean` | `true` | Included in the search. |
| [`filterable`](https://svelte-lean.vercel.app/table/api.md#column-filterable) | Sort, filter and aggregate | `boolean` | `true` | Gets a filter input when filters is on. |
| [`filter`](https://svelte-lean.vercel.app/table/api.md#column-filter) | Sort, filter and aggregate | `(value: unknown, filter: Filter, row: T) => boolean` | `matchesFilter` | Custom predicate instead of the operator matcher. |
| [`search`](https://svelte-lean.vercel.app/table/api.md#message-search) | TableMessages | `string` | `'Search rows'` | Label and placeholder of the search input. |
| [`filter`](https://svelte-lean.vercel.app/table/api.md#message-filter) | TableMessages | `(column: string) => string` | `` (column) => `Filter ${column}` `` | Label of a column filter input. |
| [`setSearch`](https://svelte-lean.vercel.app/table/api.md#table-setsearch) | Table | `(search: string) => void` | – | Sets the search text and resets the page. |
| [`setFilter`](https://svelte-lean.vercel.app/table/api.md#table-setfilter) | Table | `(id: string, value: unknown, operator?: FilterOperator) => void` | – | Replaces the filter of a column; undefined removes it. Resets the page. |
| [`matchesFilter`](https://svelte-lean.vercel.app/table/api.md#core-matchesfilter) | Core entry | `(value, filter: Filter, locale?) => boolean` | – | The default filter predicate for every FilterOperator. |
| [`Filter`](https://svelte-lean.vercel.app/table/api.md#type-filter) | Events and positions | `{ id: string; operator?: FilterOperator; value: unknown }` | – | One column filter. |
| [`FilterOperator`](https://svelte-lean.vercel.app/table/api.md#type-filteroperator) | Events and positions | `'contains' \| 'equals' \| 'startsWith' \| 'gt' \| 'gte' \| 'lt' \| 'lte' \| 'between' \| 'in' \| 'empty'` | – | Operators of matchesFilter(); 'between' takes a two-element array, 'in' an array. |

## Related

- [Sorting](https://svelte-lean.vercel.app/table/sorting.md)
- [Selection](https://svelte-lean.vercel.app/table/selection.md)
- [Localization & RTL](https://svelte-lean.vercel.app/table/localization-rtl.md)
