Sorting, filtering & search
Client-side multi-column sort on header click, per-column filters that hide rows, and quick search that highlights matches.
Click a header to sort (click again to reverse, a third click clears it); shift-click another header to add it as a secondary sort. Use Filter in the toolbar for per-column filters; the preset shows a group of two Role rows. Switch the "Or" between them to "And" to change the whole group.
For a table with search, sorting, and delete, read this page, the toolbar for the search box and filter menu, and Row operations for delete.
Sorting
Sorting is multi-column. Columns with a declared type sort with that cell type's compare
(numbers numerically, dates chronologically, select columns by choice label). Untyped columns use a numeric-aware
localeCompare. Empty cells sort last in both directions. Click a header to sort by that column.
Shift-click adds another column to the sort without clearing the first.
A column's sortCompare compares two data rows to order that column. It runs before the cell
type's compare and the default compare. A zero return defers the pair to the default compare.
The sort direction still applies.
headerClickBehavior="sort" on DataGridProvider makes a plain header click cycle through
ascending, descending, and clear. The default, "select", instead selects the whole column. See
Selection & keyboard:
<DataGridProvider headerClickBehavior="sort" {...grid} columns={columns}>Per-column sortable: false removes a column from the click-to-sort gesture, though you can
still sort it programmatically. Read sort state through useDataGridSortState() and drive it
through useDataGridActions().setSorts() or .toggleSort().
Filtering
Per-column operators include contains, notContains, equals, notEquals, startsWith,
endsWith, empty, and notEmpty, plus gt, gte, lt, lte, and isBetween for numeric and
date columns, and isAnyOf and isNoneOf for text, number, date, and select columns. A cell type can declare its own operator list
through filter.operators, which the menu offers when the column declares none. The value input
follows the cell type's filter.valueInput. isBetween
renders two typed inputs (from and to) side by side instead of one. isAnyOf renders a checkbox
list of the column's own choices and keeps a row when its value equals any checked one.
Its value is a string[], and an empty list matches no rows. On a column without choices,
the checkbox list holds the distinct values of the column.
The isNoneOf operator is the complement of isAnyOf. It keeps a row when its value equals none of
the listed ones. An empty list matches every row. Both operators compare the cell text and ignore
case. A number or date column compares its stored value as text, for example 42. The long-text, url, and
checkbox columns do not offer them, unless the column lists them in filterOperators.
The header filter puts a set filter in each column header. See
the toolbar add-on. It writes isAnyOf or isNoneOf,
whichever list is shorter.
DataGridFilterMenu is a popover that lists the filter tree. The top level joins its entries with
"and". A filter group New is a bordered box with its own rows. The first row of a group reads
"Where". Every later row shows a select with "And" and "Or". The select sets the join of the whole
group, so all its rows show the same word. A group with one row has no select yet. A group holds
one more group at most. Each row shows a column, an operator, a value, a trash button, and a drag
handle. "Add filter", "Add group", and "Clear all" sit at the foot of the menu, and each
group has its own "Add filter" and "Add group". Per-column filterable: false excludes a column from
the menu's column picker. Drag the drag handle, or reorder through the keyboard
(toolbar add-on: filter menu keyboard & accessibility),
to move a row or group among its siblings. While you drag, a line shows where the row or group lands.
To build "(Name contains A or B) and Country = X" in the menu, add a filter for Country, add a group, and give the group two Name rows.
To set filters from code, pass FilterNode entries to useDataGridActions().setFilters(). A
FilterNode is a FilterSpec row or a FilterGroup:
actions.setFilters([
{ columnId: "country", operator: "equals", value: "X" },
{
join: "or",
filters: [
{ columnId: "name", operator: "contains", value: "A" },
{ columnId: "name", operator: "contains", value: "B" },
],
},
]);A row survives when every top-level node matches. A group with join: "and" matches when every
child matches. A group with join: "or" matches when any child matches. A group with any other
join value matches like "and". An empty group matches every row. A flat list of FilterSpec
entries is a valid filterState. setFilters New drops a group nested deeper than 2 levels, with its rows.
A FilterSpec has these fields:
| Field | Type | Meaning |
|---|---|---|
columnId | string | The id of the column to filter. |
operator | FilterOperator | One of the operator names listed above. |
value | string, [string, string], or string[] | The text to compare, even for number and date columns. isBetween takes [from, to]. isAnyOf and isNoneOf take a list of cell texts. empty and notEmpty take none. |
filterId | string | Optional. A stable key for the filter row. |
A FilterGroup New has these fields:
| Field | Type | Meaning |
|---|---|---|
join | "and" or "or" | How the group combines its children. |
filters | FilterNode[] | The rows and the one level of groups inside. |
filterId | string | Optional. A stable key for the group. |
DataGridFilterMenu and DataGridSortList offer the grid's visible columns by default. Pass
allColumns to include hidden columns. A filter on a hidden column keeps applying. Without
allColumns, the menu shows that row as a raw column id and logs a dev-time warning.
Two column-level fields replace the built-in behavior. filterMatch(value, filter) is a
per-column predicate over the cell's RAW value (not its stringified form) that decides whether
the row passes; it replaces the built-in matcher for that column, and a thrown predicate fails the
row. filterOperators replaces the operator list the filter menu offers for that column, for
example the comparison operators for a consumer-registered numeric cell type whose type key is
not number or date. The same two fields exist on a cell type as filter.match and
filter.operators, and the column's own fields win over them:
const columns = defineColumns<Invoice>()([
{
id: "total",
header: "Total",
accessorKey: "total",
type: "currency",
filterOperators: ["contains", "gt", "gte", "lt", "lte", "isBetween"],
filterMatch: (value, filter) => typeof value === "number" && value > 100,
},
]);DataGridFilterMenu's operatorsForColumn prop overrides the operator list per render, ahead of
filterOperators. See Toolbar: the pieces.
DataGridFilterMenu, along with the search input and columns menu below, comes from the
toolbar add-on, which owns the install, wiring, toolbar order,
and the filter menu's keyboard & accessibility table.
Search
Search highlights matches and jumps to them. It never hides rows. A filter hides rows.
DataGridSearch is a quick-search input across visible columns, with match highlighting and
next and previous navigation that shows a current/total counter. Navigating to a match moves
the active cell there and scrolls it into view, so results are never left off-screen.
Search matches each cell's quick-search text: the type's toSearchText when the type declares
it, else its toText. A column without a declared type searches the raw String(value). The
built-in select type declares toSearchText, so a select column searches the choice LABEL.
Its filters match the stored value. Full semantics on
custom cell types.
It debounces typing before it writes the store's search text. Pass debounceMs to change the
200 ms default.
actions.setSearch(text, { matchCase, wholeCell }) sets the search options. matchCase respects
letter case and wholeCell matches only cells that equal the search text. Both default to
false, and they stay set across later searches. The find & replace
panel uses them.
It never narrows the row set. Search highlights and navigates matches over the rows already in
data. viewIndex is untouched, so everything that walks viewIndex (export with
scope: "view", select-all, pinned-row aggregates) sees the same rows with or without a search
term. To actually remove rows, use a filter.
Search is capped at 1000 matches
Beyond the cap, the counter shows a distinct label instead of a total. See
labels.toolbar.searchMatchesCapped.
Why not the browser's own Ctrl+F?
Native find-in-page only sees what is in the DOM, and a virtualized grid renders only the rows
in view. DataGridSearch searches the full dataset and scrolls to whatever it finds.
captureFindShortcut is on by default — and strictly grid-scoped
When DataGridSearch is mounted, pressing Mod+F is only ever intercepted
while focus is somewhere inside the grid or its toolbar. Move focus anywhere else on the
page, such as your own page-level search box, a different widget, or the body, and mod+F is
left completely alone. The browser's native find opens exactly as if this prop did not exist.
When several grids share one page, mod+F always reaches the grid that holds focus.
A toolbar you build yourself counts as part of the grid when it carries the data-grid-toolbar
attribute and contains the DataGridSearch.
If you have your own search UI elsewhere on the page, you do not need to do anything to keep
it safe.
Enter in the input moves to the next match, and Shift+Enter moves to the previous one. Escape clears the text.
Pressing Mod+F again while the input is already focused selects its text, matching how browser find-bars behave when re-triggered.
To opt out and always defer to the browser's native find:
<DataGridSearch captureFindShortcut={false} />Sort list (toolbar add-on)
Click-to-sort headers work without any add-on. For a toolbar button and popover to add, remove, and reorder sorts without touching the headers, see the sort list add-on.
Initial sort, filter, and search
defaultSortState, defaultFilterState, and defaultSearchText seed the
store once at creation. The store owns the state afterwards, and later changes to these props are
ignored:
<DataGrid
defaultData={rows}
columns={columns}
defaultSortState={[{ columnId: "age", direction: "desc" }]}
/>A controlled prop (sortState and so on) wins over its default.
Controlled mode (server-side sort/filter)
By default, sortState, filterState, and searchText are internal, uncontrolled store
state. Read them through useDataGridSortState(), useDataGridFilterState(), and
useDataGridSearchText(), and write them through useDataGridActions(). The grid sorts,
filters, and searches against the data you passed, in-process. Each has a controlled prop pair
on DataGridProvider or DataGrid: sortState/onSortChange, filterState/onFilterChange,
and searchText/onSearchTextChange, for a "send the spec to the server, render whatever comes
back as the view" mode. This follows the same controlled-input convention as a plain
<input value onChange>, where the prop, once you pass it, is the source of truth, and
onXChange fires on every user-driven change regardless. See the "controlled pair" row of the
Events & state three-shapes table for
the canonical definition, and Recipes for the full
server-side pattern.
When data holds only part of the result set, set sortMode="server": the grid tracks the sort spec and never reorders data. See Lazy loading.