sveltelean Table
Versionv0.2.0 GitHub

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
<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
// 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
// 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 and search
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
Page 1 of 5 · 36 rows
{"search":"","filters":[]}

Filtering.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

NameOfTypeDefaultDescription
searchableControlsbooleanfalseShows the search input. state.search applies without it, so a search box elsewhere in the page can write the state.
searchTextControls(row: T) => stringundefinedThe text search terms are matched against, per row. Without it: the values of the searchable, visible columns.
searchDelayControlsnumber0Milliseconds between the last keystroke in the search box or a filter input and the state change; clearing a field applies at once.
filtersControlsbooleanfalseAdds a filter input under each filterable leaf header (operator contains).
localePresentationstringundefinedLocale of the collator used for sorting and of case folding in search and filters.
searchTableStatestring''The search text; terms are split on whitespace.
filtersTableStateFilter[][]Column filters with operator and value.
searchableSort, filter and aggregatebooleantrueIncluded in the search.
filterableSort, filter and aggregatebooleantrueGets a filter input when filters is on.
filterSort, filter and aggregate(value: unknown, filter: Filter, row: T) => booleanmatchesFilterCustom predicate instead of the operator matcher.
searchTableMessagesstring'Search rows'Label and placeholder of the search input.
filterTableMessages(column: string) => string(column) => `Filter ${column}`Label of a column filter input.
setSearchTable(search: string) => void–Sets the search text and resets the page.
setFilterTable(id: string, value: unknown, operator?: FilterOperator) => void–Replaces the filter of a column; undefined removes it. Resets the page.
matchesFilterCore entry(value, filter: Filter, locale?) => boolean–The default filter predicate for every FilterOperator.
FilterEvents and positions{ id: string; operator?: FilterOperator; value: unknown }–One column filter.
FilterOperatorEvents and positions'contains' | 'equals' | 'startsWith' | 'gt' | 'gte' | 'lt' | 'lte' | 'between' | 'in' | 'empty'–Operators of matchesFilter(); 'between' takes a two-element array, 'in' an array.