gridcn

Sorting, filtering & search

Client-side multi-column sort, per-column filters, and quick search with match navigation.

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.

Name
Email
Age
Role
Score
Silent Tiger
user0@example.com
63
Viewer
4
Quick Lion
user1@example.com
67
Viewer
89
Swift Wolf
user2@example.com
40
Manager
80
Eager Lion
user3@example.com
50
User
51
Eager Tiger
user4@example.com
45
Viewer
83
Silent Wolf
user5@example.com
24
User
15
Swift Lion
user6@example.com
34
Viewer
62
Eager Lion
user7@example.com
21
Manager
42
Eager Eagle
user8@example.com
25
Editor
35
Bold Tiger
user9@example.com
19
Editor
52
Bold Bear
user10@example.com
42
User
28
Swift Wolf
user11@example.com
47
Manager
9
Swift Bear
user12@example.com
41
Viewer
45
Eager Wolf
user13@example.com
35
Editor
44
Swift Eagle
user14@example.com
65
Viewer
51
Quick Lion
user15@example.com
22
Manager
28
Bold Wolf
user16@example.com
30
Editor
76
Swift Eagle
user17@example.com
19
Admin
26
Eager Eagle
user18@example.com
35
Manager
70
Eager Bear
user19@example.com
33
Editor
67
Silent Lion
user20@example.com
48
Manager
89
Silent Tiger
user21@example.com
62
Manager
40
Eager Tiger
user22@example.com
51
Manager
82
Silent Lion
user23@example.com
26
Admin
16
Bold Eagle
user24@example.com
26
Admin
38
Bold Lion
user25@example.com
49
Viewer
3
Quick Tiger
user26@example.com
26
Viewer
41
Bold Eagle
user27@example.com
50
Editor
10
Swift Tiger
user28@example.com
55
Editor
6
Silent Lion
user29@example.com
35
Viewer
42

Sorting

Sorting is multi-column. Columns with a declared type sort with that cell type's compare (numbers numerically, dates chronologically); 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.

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 for select columns. isBetween renders two typed inputs (from and to) side by side instead of one. isAnyOf renders a checkbox list of the select 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. DataGridFilterMenu is a popover that lists active filters as aligned rows (join cell, column, operator, value, trash, drag handle) with add and reset controls. Per-column filterable: false excludes a column from the menu's column picker. Row order is array order in FilterSpec[]. Drag the grip, or reorder through the keyboard (see below), to change it.

DataGridFilterMenu, along with the search input and columns menu below, comes from the data-grid-toolbar add-on.

pnpm dlx shadcn@latest add @gridcn/data-grid-toolbar
import {
  DataGridToolbar,
  DataGridSearch,
  DataGridFilterMenu,
  DataGridColumnsMenu,
} from "@/components/data-grid-toolbar/data-grid-toolbar";

<DataGridProvider {...grid} columns={columns}>
  <DataGridToolbar>
    <DataGridSearch />
    <DataGridFilterMenu />
    <DataGridColumnsMenu />
  </DataGridToolbar>
  <DataGridRoot>
    <DataGridHeader />
    <DataGridBody />
  </DataGridRoot>
</DataGridProvider>;

Each toolbar piece is independent

Drop DataGridFilterMenu if you only want search, and add your own buttons alongside them.

Keyboard & accessibility

KeyAction
TabMoves through a row's controls, in order: column select, operator select, value, remove, drag handle
ArrowUp / ArrowDown (grip focused)Moves that filter row one position up/down
Space / Enter (grip focused)Picks up/drops the row for pointer-equivalent keyboard dragging (@dnd-kit/react's keyboard sensor)
Backspace / DeleteRemoves the focused filter row

Plain ArrowUp or ArrowDown on a focused grip always moves the row by exactly one position. The keyboard sensor of @dnd-kit/react also offers its own gesture for that key, but gridcn checks the plain arrow press first, and it wins. Every reorder announces the change through a polite aria-live region (labels.toolbar.filterReorderAnnouncement, for example "Name filter moved to position 2 of 3"). Focus never gets lost. After a reorder, focus stays on the moved row's grip. After deleting a row, focus moves to the next row's grip, or to the "Add filter" button once the list is empty.

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.

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 can only see what is in the DOM, and a virtualized grid only ever renders the rows in view. Everything outside the window does not exist as a node, so the browser's find bar cannot locate it, no matter how large the dataset really is. DataGridSearch does not have that problem. It 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. If you have your own search UI elsewhere on the page, you do not need to do anything to keep it safe.

DataGridSearch makes itself the "feels native" stand-in for browser find automatically. Pressing Mod+F while focus is anywhere inside the grid or its toolbar focuses this input instead of opening the browser's find bar (preventDefault, so native find never opens). Pressing it 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, but data-grid-sort-list gives users a toolbar button and popover to add, remove, and reorder sorts without touching the headers. This is useful when columns are hidden or narrow, or when you prefer not to rely on shift-click for multi-column sort:

Name
Email
Role
Score
Silent Tiger
user0@example.com
Viewer
4
Quick Lion
user1@example.com
Viewer
89
Swift Wolf
user2@example.com
Manager
80
Eager Lion
user3@example.com
User
51
Eager Tiger
user4@example.com
Viewer
83
Silent Wolf
user5@example.com
User
15
Swift Lion
user6@example.com
Viewer
62
Eager Lion
user7@example.com
Manager
42
Eager Eagle
user8@example.com
Editor
35
Bold Tiger
user9@example.com
Editor
52
Bold Bear
user10@example.com
User
28
Swift Wolf
user11@example.com
Manager
9
Swift Bear
user12@example.com
Viewer
45
Eager Wolf
user13@example.com
Editor
44
Swift Eagle
user14@example.com
Viewer
51
Quick Lion
user15@example.com
Manager
28
Bold Wolf
user16@example.com
Editor
76
Swift Eagle
user17@example.com
Admin
26
Eager Eagle
user18@example.com
Manager
70
Eager Bear
user19@example.com
Editor
67
Silent Lion
user20@example.com
Manager
89
Silent Tiger
user21@example.com
Manager
40
Eager Tiger
user22@example.com
Manager
82
Silent Lion
user23@example.com
Admin
16
Bold Eagle
user24@example.com
Admin
38
Bold Lion
user25@example.com
Viewer
3
Quick Tiger
user26@example.com
Viewer
41
Bold Eagle
user27@example.com
Editor
10
Swift Tiger
user28@example.com
Editor
6
Silent Lion
user29@example.com
Viewer
42
pnpm dlx shadcn@latest add @gridcn/data-grid-sort-list
import { DataGridSortList } from "@/components/data-grid-sort-list/data-grid-sort-list";

<DataGridProvider {...grid} columns={columns}>
  <DataGridToolbar>
    <DataGridSortList />
  </DataGridToolbar>
  <DataGridRoot>
    <DataGridHeader />
    <DataGridBody />
  </DataGridRoot>
</DataGridProvider>;

Each row has a column select, an ascending/descending select, a remove button, and a drag handle. Precedence is array order in SortSpec[], changed by dragging the grip (@dnd-kit/react) or through the keyboard (see below). It reads and writes the same useDataGridSortState() and useDataGridActions().setSorts() as click-to-sort headers, so the two UIs stay in sync automatically. The button shows a count badge once one or more sorts are applied, matching the badge of DataGridFilterMenu.

Keyboard & accessibility

KeyAction
TabMoves through a row's controls, in order: column select, direction select, remove, drag handle
ArrowUp / ArrowDown (grip focused)Moves that sort row one position up/down, which changes precedence
Space / Enter (grip focused)Picks up/drops the row for pointer-equivalent keyboard dragging (@dnd-kit/react's keyboard sensor)
Backspace / DeleteRemoves the focused sort row

Plain ArrowUp or ArrowDown on a focused grip always moves the row by exactly one position. The keyboard sensor of @dnd-kit/react also offers its own gesture for that key, but gridcn checks the plain arrow press first, and it wins. Every reorder announces the change through a polite aria-live region (labels.sort.sortReorderAnnouncement, for example "Age sort moved to position 1 of 2"). Focus never gets lost. After a reorder, focus stays on the moved row's grip. After deleting a row, focus moves to the next row's grip, or to the "Add sort" button once the list is empty.

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.

On this page