Sorting
Every data column sorts by default, with a function that suits its kind. Click a header to sort by it; shift-click another header to sort by both.
Notes | ||||||
|---|---|---|---|---|---|---|
| Sofia Nguyen | sofia.nguyen@mail.test | Active | 49 | $3,871.00 | Jun 14, 2026 | — |
| Saanvi Iyer | saanvi.iyer@corp.example | Invited | 46 | $3,634.00 | Aug 3, 2023 | Speaks at meetups. |
| Mateo Iyer | mateo.iyer@corp.example | Active | 46 | $3,634.00 | Oct 21, 2023 | — |
| Freya Patel | freya.patel@mail.test | Active | 45 | $3,555.00 | Apr 29, 2025 | Asked about invoices. |
| Mateo Nguyen | mateo.nguyen@corp.example | Invited | 44 | $3,476.00 | Oct 28, 2026 | — |
| Idris Müller | idris.muller@mail.test | Paused | 60 | $1,740.00 | Sep 9, 2024 | Renewal in Q1. |
| Olivia Nguyen | olivia.nguyen@example.com | Invited | 54 | $1,566.00 | Sep 8, 2025 | — |
| Rohan Silva | rohan.silva@team.dev | Active | 19 | $1,501.00 | Jan 30, 2026 | Speaks at meetups. |
| Sofia Tanaka | sofia.tanaka@team.dev | Active | 47 | $1,363.00 | May 29, 2024 | Asked about invoices. |
| Olivia Reddy | olivia.reddy@example.com | Active | 17 | $1,343.00 | Mar 7, 2025 | — |
import { DataGrid, createDataGridColumns, useDataGrid } from "@/components/ui/data-grid";
import { makePeople, STATUS_OPTIONS, type Person } from "@/lib/data-grid-demo-data";
const col = createDataGridColumns<Person>();
const columns = col.columns([ col.text("name", { header: "Name", size: 180 }), // Shortest to longest rather than A to Z: a sort function gets the two rows and the column id. col.text("email", { header: "Email", size: 240, sortFn: (a, b, id) => a.getValue<string>(id).length - b.getValue<string>(id).length, }), col.enum("status", { header: "Status", options: STATUS_OPTIONS, size: 120 }), // Numbers sort descending on the first click; this one starts ascending. col.number("seats", { header: "Seats", sortDescFirst: false, size: 90 }), col.currency("mrr", { header: "MRR", currency: "USD", size: 120 }), col.date("joinedAt", { header: "Joined", size: 130 }), // Free text has no useful order. col.text("notes", { header: "Notes", enableSorting: false, size: 220 }),]);
const data = makePeople(80, 12);
/** Click a header to sort, shift-click another to add a second key. Starts sorted by MRR, highest first. */export default function DataGridSorting() { const grid = useDataGrid<Person>({ name: "sorting", data, columns, getRowId: (row) => row.id, selection: false, initialState: { sorting: [{ id: "mrr", desc: true }] }, }); return <DataGrid grid={grid} />;}Use it
Section titled “Use it”There is nothing to turn on. Sorting is on for every column with an accessor, and the kind picks
the function: basic for numbers, currencies, percentages and booleans, datetime for dates,
alphanumeric for everything else.
From the header
Section titled “From the header”Click the title to cycle through the directions. The first direction depends on the column:
text starts ascending, numbers and dates start descending (TanStack’s auto direction, which
sortDescFirst overrides). The second click flips it, the third clears it.
Shift-click a header to add it to the current sort instead of replacing it. With more than one column sorted, each header shows a small badge with its position in the order. Shift-clicking a sorted column cycles it in place and, at the end of the cycle, drops it from the order.
The arrow beside the title shows the direction; an up-down chevron appears on hover when the column
is sortable but not sorted. The header is a button, so Enter or Space sorts it from the keyboard and
Shift+Enter adds it to a multi-sort. Each change is announced to screen readers and aria-sort is
kept on the header.
From the column menu
Section titled “From the column menu”The ⋮ menu (or a right-click on the header) has Sort ascending, Sort descending and, once the column is sorted, Clear sort. The two sort items set the column as the only sort, replacing a multi-sort; the one in effect is marked “applied”.
Start sorted
Section titled “Start sorted”<DataGrid data={data} columns={columns} initialState={{ sorting: [{ id: "mrr", desc: true }] }} />sorting is a list of { id, desc } in priority order, so two entries start a multi-sort. With
persist on, the user’s last sort wins over initialState.
Per column
Section titled “Per column”col.text("notes", { enableSorting: false }) // not sortablecol.number("seats", { sortDescFirst: false }) // first click sorts ascendingcol.text("name", { sortFn: "textCaseSensitive" }) // a different built-incol.number("rank", { invertSorting: true }) // 1st, 2nd, 3rd: lower is bettercol.date("closedAt", { sortUndefined: "last" }) // blanks at the end either wayA custom sortFn gets the two rows and the column id and returns a number, like Array.sort:
col.text("email", { sortFn: (a, b, id) => a.getValue<string>(id).length - b.getValue<string>(id).length,})How it behaves
Section titled “How it behaves”- Sorting runs on the filtered rows, before grouping and pagination, so a sort reorders the whole result and the pager goes back to page 1.
- With grouping on, the grouped column moves to the front and sorting it orders the groups; sorting another column orders the rows inside each group.
- Sorting sees the raw value, not the formatted one: a
currencycolumn sorts numerically, adatecolumn by time, anenumcolumn by its value rather than its label. Give the column asortFnwhen the label order is the one people expect. sortUndefineddefaults to1: blank values go last in an ascending sort and first in a descending one.enableSortingRemovalis on, so the cycle always has a “no sort” step. Settable={{ enableSortingRemoval: false }}to keep a column sorted once it is clicked, ortable={{ enableMultiSort: false }}to make shift-click behave like a click.- The sort state lives in
grid.table.state.sorting;grid.table.setSorting(...)andcolumn.toggleSorting(desc, multi)change it from your own controls.
Keyboard
Section titled “Keyboard”| Key | On a header | What it does |
|---|---|---|
| Enter / Space | Cycle the sort | |
| Shift + Enter | Add the column to the sort, or cycle it in place | |
| Shift + click | The same with the pointer |
Options
Section titled “Options”Column options
Section titled “Column options”| Option | Type | Default | What it does |
|---|---|---|---|
enableSorting |
boolean |
true |
false removes the sort from the header, the menu and the keyboard. |
sortFn |
string or function | by kind | How two rows compare: "alphanumeric", "alphanumericCaseSensitive", "text", "textCaseSensitive", "datetime", "basic", or (rowA, rowB, columnId) => number. alphanumeric splits digits from letters and compares numbers numerically; text is a plain string compare; basic compares the raw values; datetime compares times. |
sortDescFirst |
boolean |
auto: true for non-string values |
The direction of the first click. |
sortUndefined |
false, -1, 1, "first" or "last" |
1 |
Where blanks go. false passes them to the sort function. |
invertSorting |
boolean |
false |
Flip the result, for scales where lower is better. |
enableMultiSort |
boolean |
true |
false keeps this column out of a multi-sort. |
Grid options
Section titled “Grid options”Set these through the table escape hatch, table={{ ... }} on <DataGrid> or table: { ... }
on useDataGrid.
| Option | Type | Default | What it does |
|---|---|---|---|
enableSorting |
boolean |
true |
Sorting for the whole grid. |
enableMultiSort |
boolean |
true |
Shift-click adds a key. |
enableSortingRemoval |
boolean |
true |
The cycle includes “no sort”. |
enableMultiRemove |
boolean |
true |
A shift-click cycle can drop the column. |
maxMultiSortColCount |
number |
unlimited | Cap on sort keys. |
sortDescFirst |
boolean |
auto | The first direction for every column. |
isMultiSortEvent |
(e) => boolean |
e.shiftKey |
Which click or key adds a key. |
initialState.sorting |
{ id: string; desc: boolean }[] |
[] |
The starting sort. |
See also
Section titled “See also”- Grouping and aggregation: how a sort orders groups.
- URL state: keep the sort in the address bar.
- Server-side data: let the server sort.