Toolbar
The data-grid-toolbar add-on: a flex frame for search, filter menu, sort list, columns menu, and your own buttons.
data-grid-toolbar is a flex frame for the grid's toolbar slots: quick search, the filter menu,
the sort list, the columns menu, and your own buttons. It renders children left to right in the
order you write them, and each piece is independent. Drop one and the rest keep their relative
order.
pnpm dlx shadcn@latest add @gridcn/data-grid-toolbar
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.
Wiring
The toolbar sits between the provider and the root, so it needs the split form of the grid
(DataGridProvider, DataGridRoot, DataGridHeader, DataGridBody) instead of <DataGrid>.
useDataGridState comes from the undo & redo add-on. Any
controlled data plus onDataChange works in its place:
"use client";
import {
DataGridProvider,
DataGridRoot,
DataGridHeader,
DataGridBody,
} from "@/components/data-grid/data-grid";
import { useDataGridState } from "@/components/data-grid-history/data-grid-history";
import {
DataGridToolbar,
DataGridSearch,
DataGridFilterMenu,
DataGridColumnsMenu,
} from "@/components/data-grid-toolbar/data-grid-toolbar";
import { columns, initialRows } from "./columns";
export function PeopleGrid() {
const grid = useDataGridState(initialRows, { getRowId: (row) => row.id });
return (
<DataGridProvider {...grid} columns={columns}>
<DataGridToolbar>
<DataGridSearch />
<DataGridFilterMenu />
<DataGridColumnsMenu />
</DataGridToolbar>
<DataGridRoot className="h-[400px]">
<DataGridHeader />
<DataGridBody />
</DataGridRoot>
</DataGridProvider>
);
}Standard toolbar order
The gridcn convention is: DataGridSearch first (leftmost. Finding data comes before
anything else), then the row-view modifiers DataGridFilterMenu and DataGridSortList, then
the column configuration DataGridColumnsMenu, then data movement DataGridImportButton
before DataGridExportButton (data flows in, then out), and your own utility buttons
(shortcuts, density, and the like) last.
The pieces
DataGridSearch: quick search with match navigation. The behavior (capping,mod+Fcapture) is documented on Sorting, filtering & search: search.DataGridFilterMenu: the per-column filter popover. The operator set and filter semantics are on Sorting, filtering & search: filtering. PassoperatorsForColumn(column)to replace the per-column operator list; resolution order is this prop, then the column's ownfilterOperators, then the cell type'sfilter.operators, then the built-in default for the type's name.DataGridSortList: the sort management popover, its own page.DataGridColumnsMenu: show and hide only. Pin controls live in the header menu, not here (see Columns: visibility).
Filter Name for only equals/not equals, or Score for only greater/less than. Sort by Email even though it is hidden from the grid.
Filter menu keyboard & accessibility
| Key | Action |
|---|---|
| Tab | Moves through a row's controls, in order: column select, operator select, value, remove, drag handle |
| ArrowUp / ArrowDown (drag handle focused) | Moves that filter row one position up/down |
| Space / Enter (drag handle focused) | Picks up/drops the row for pointer-equivalent keyboard dragging (@dnd-kit/react's keyboard sensor) |
| Backspace / Delete (drag handle focused) | Removes that filter row |
Plain ArrowUp or ArrowDown on a focused drag handle 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 drag handle.
After deleting a row, focus moves to the next row's drag handle, or to the "Add filter" button once the
list is empty.
Header filter
NewA column header can hold its own filter button. The button opens a popover with a searchable list of the column's values, and the user checks the values to keep. DataGridHeaderFilter is that button. Pass it to the renderHeaderFilter slot of DataGridRoot.
The slot receives { column, index }. Spread it into the component:
import { DataGridHeaderFilter } from "@/components/data-grid-toolbar/data-grid-toolbar";
<DataGridRoot renderHeaderFilter={(ctx) => <DataGridHeaderFilter {...ctx} />}>
<DataGridHeader />
<DataGridBody />
</DataGridRoot>The button shows on each column that offers isAnyOf or isNoneOf and is not filterable: false. By default these are the text, number, date, and select columns. The button stays hidden until hover or focus. While the column has a filter, the button stays visible and the header cell carries data-filtered.
The popover works like this:
- The list holds one entry per distinct value, with the number of rows that have it. Values that differ only in case form one entry, and Apply writes the first spelling it meets. An empty value shows as "(Blanks)".
- The search box New narrows the list. The popover opens with focus in it. "Select all" is a checkbox at the start of the search field. It checks or unchecks the values that match the typed text. It comes before the search input in the Tab order, and Space toggles it. Its box is empty when none of those values is checked, shows the indeterminate mark when some are, and shows a check when all are.
- The status line New under the search reads "N of M selected". N is the number of checked values and M is the number of values in the list. Set the text with the
toolbar.headerFilterSelectedCountlabel. The counts of the entries sit in a right-aligned column. - The counts include only the rows that pass every other filter. The filter of the column itself never hides its own values. If the join is
or, the counts include all rows. - Apply stores the shorter list. If fewer values are checked than unchecked, it writes
isAnyOfwith the checked values. Otherwise it writesisNoneOfwith the unchecked values. A value that enters the data later passesisNoneOf. - Apply keeps every other filter, including condition filters on the same column.
- Apply is disabled while no value is checked, and while the stored list would hold more than 1,000 entries. A message explains the second case.
- Clear removes every filter of the column. Escape or a press outside the popover discards the selection.
- The list reads at most 100,000 distinct values. Past that limit it shows the first values and a message.
The filter works in a read-only grid, because filtering does not write data.
| Key | Action |
|---|---|
| Shift+Alt+ArrowUp | Opens the popover of the active cell's column. Focus returns to the grid when the popover closes |
| ArrowUp / ArrowDown / Home / End (list) | Moves between values |
| Space (list) | Checks or unchecks the value |
| Enter (list or search box) | Applies the selection |
The filter menu uses the same list for isAnyOf and isNoneOf on a column without choices.
DataGridHeaderFilter takes the two values that the slot passes:
| Prop | Type | Default | Meaning |
|---|---|---|---|
column | AnyColumnDef | required | The column that the button filters. |
index | number | required | The index of the column in the header. |
With lazy rows or sortMode="server", the list shows only the values of the loaded rows. A value
that exists only on the server is missing from the list. Your server decides which rows the
grid loads, so it decides which values the list can show. The popover then shows the hint
"From loaded rows". Server pagination with client sorting loads whole pages, so the hint stays hidden. Set the text with the toolbar.headerFilterLoadedRowsOnly label.
Related
- Sorting, filtering & search: the core behavior the toolbar controls
- Context menu: the other per-column surface
- Columns: the
renderHeaderFilterandrenderHeaderMenuslots - URL state: shares the stored filter in the URL