Table State
Filtering
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.
- Fixture
- Benchmark
On this page
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.
<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}
/>// 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
]// 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.
Amara Okafor | Active | Engineering | Europe | $4,501 | 20% | 2026-01-07 |
Jonas Lindqvist | Active | Design | Americas | $12,420 | 33% | 2026-06-18 |
Mei Tanaka | Away | Operations | Asia | $20,339 | 46% | 2026-02-01 |
Rafael Duarte | Invited | Growth | Europe | $28,258 | 59% | 2026-07-12 |
Selin Aydın | Active | Engineering | Americas | $36,177 | 72% | 2026-03-23 |
Noah Brenner | Active | Design | Asia | $44,096 | 85% | 2026-08-06 |
Priya Raman | Away | Operations | Europe | $4,015 | 98% | 2026-04-17 |
Elias Moreau | Invited | Growth | Americas | $11,934 | 30% | 2026-09-28 |
{"search":"","filters":[]} <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 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 | 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 | Controls | (row: T) => string | undefined | The text search terms are matched against, per row. Without it: the values of the searchable, visible columns. |
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 | Controls | boolean | false | Adds a filter input under each filterable leaf header (operator contains). |
locale | Presentation | string | undefined | Locale of the collator used for sorting and of case folding in search and filters. |
search | TableState | string | '' | The search text; terms are split on whitespace. |
filters | TableState | Filter[] | [] | Column filters with operator and value. |
searchable | Sort, filter and aggregate | boolean | true | Included in the search. |
filterable | Sort, filter and aggregate | boolean | true | Gets a filter input when filters is on. |
filter | Sort, filter and aggregate | (value: unknown, filter: Filter, row: T) => boolean | matchesFilter | Custom predicate instead of the operator matcher. |
search | TableMessages | string | 'Search rows' | Label and placeholder of the search input. |
filter | TableMessages | (column: string) => string | (column) => `Filter ${column}` | Label of a column filter input. |
setSearch | Table | (search: string) => void | – | Sets the search text and resets the page. |
setFilter | Table | (id: string, value: unknown, operator?: FilterOperator) => void | – | Replaces the filter of a column; undefined removes it. Resets the page. |
matchesFilter | Core entry | (value, filter: Filter, locale?) => boolean | – | The default filter predicate for every FilterOperator. |
Filter | Events and positions | { id: string; operator?: FilterOperator; value: unknown } | – | One column filter. |
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. |