Sorting, Grouping & Summaries
Sort by clicking column headers, group rows by any groupable column, and aggregate numeric columns with summary rows.
Sorting & Grouping
Every data column sorts on a header click, cycling ascending, descending, unsorted — or
descending first for a column that declares sortDescFirst: true: a date, a rating, a count. One
column at a time: a click on another header moves the sort there. sortable: false takes that away, and a synthetic
column (one with no accessor) has no value to sort by
and never had it.
Start Sorted
viewDefaults.sort is the view's baseline: the header indicator shows it, and users can still change or clear it.Name | Department | Salary |
|---|---|---|
Sofia Martinez | Platform | 165000 |
Alexander Novak | Data | 148000 |
Emma Wilson | Platform | 142000 |
Isabella Singh | Design | 140000 |
Mia Zhang | Product | 135000 |
<Table
{items}
{columns}
viewDefaults={{ sort: { column: 'salary', direction: 'desc' }, pageSize: 5 }}
/>A sort is view state: a shared link or a view restored from storage carries it, and either one overrides this baseline. URL State & Persistence sets out which of the three wins.
Grouping is opt-in: bucketing an email or a free-text note makes one group per row, so it is
not offered to every column that holds a value. groupable: true says yes, groupable: false says no, and with neither set an
explicit sortable: true grants it. A column that
declares nothing at all sorts, but does not group. Both flags sit on the column, beside the
rest of its properties.
A virtualized table never groups: the menu entry goes, and a grouping arriving from the view defaults, a URL or storage renders ungrouped — the value itself stays on the view.
Grouping with Custom Order
groupOrder names the groups that come first; every other group follows in the order its rows arrive, and a name with no rows is skipped. Rows with no value in that column land in a group called Unassigned.Name | Role | Department | Location | |
|---|---|---|---|---|
Platform (4 items) | ||||
Emma Wilson | Staff Engineer | Platform | Berlin | |
Sofia Martinez | Engineering Manager | Platform | Munich | |
Noah Kim | DevOps Engineer | Platform | Hamburg | |
Ethan Müller | QA Engineer | Platform | Hamburg | |
Product (3 items) | ||||
James Park | Frontend Developer | Product | Remote | |
Lucas Weber | Backend Developer | Product | Berlin | |
Mia Zhang | Product Manager | Product | Remote | |
Design (3 items) | ||||
Liam Chen | Product Designer | Design | Hamburg | |
Olivia Brown | UX Researcher | Design | Munich | |
Isabella Singh | Design Lead | Design | Berlin | |
Data (2 items) | ||||
Aisha Patel | Data Scientist | Data | Berlin | |
Alexander Novak | ML Engineer | Data | Munich | |
Platform (4 items)
Product (3 items)
Design (3 items)
Data (2 items)
<Table
{items}
{columns}
viewDefaults={{ groupBy: 'department' }}
groupOrder={['Platform', 'Product', 'Design', 'Data']}
/>Summaries
Summary rows aggregate a column across rows. When the table is grouped, each group gets its
own summary row; without grouping, a single total row is appended below the data. Pass prefs={{ defaults: { summaries: […] } }} to enable summaries declaratively. Users can also add and remove them at runtime via the header menu or the SmartFilterBar's summary control.
Summaries are a preference, not a view setting. Sorting and grouping decide which rows a reader sees, so they live on the view and travel in a shared link. A
summary row changes how the same rows are presented, so it belongs to this reader on this
device and stays in web storage: viewDefaults for the former, prefs for the latter.
Per-Group Summaries
Name | Department | Salary | |
|---|---|---|---|
Platform (4 items) | |||
Emma Wilson | Platform | 142000 | |
Sofia Martinez | Platform | 165000 | |
Noah Kim | Platform | 115000 | |
Ethan Müller | Platform | 85000 | |
∑ 507000 | |||
Design (3 items) | |||
Liam Chen | Design | 98000 | |
Olivia Brown | Design | 88000 | |
Isabella Singh | Design | 140000 | |
∑ 326000 | |||
Product (3 items) | |||
James Park | Product | 92000 | |
Lucas Weber | Product | 105000 | |
Mia Zhang | Product | 135000 | |
∑ 332000 | |||
Data (2 items) | |||
Aisha Patel | Data | 128000 | |
Alexander Novak | Data | 148000 | |
∑ 276000 | |||
Platform (4 items)
Summary for Platform
Design (3 items)
Summary for Design
Product (3 items)
Summary for Product
Data (2 items)
Summary for Data
<Table
{items}
{columns}
viewDefaults={{ groupBy: 'department', pageSize: 12 }}
prefs={{ defaults: { summaries: [{ column: 'salary', type: 'sum' }] } }}
/>A summary is { column, type } plus an optional formatter: (value: number) => string; the full
shape is SummaryConfig. type is sum, avg, min, max or count, and count is the one that does not do arithmetic: it counts
the rows that have a value at all. The four others skip rows whose value is not a number, and
show a dash when none is.
A summary covers every row matching the current search and filters, not the page on screen. The pager moves under a total that stays put. A table that leaves the work to the server only holds the page it was handed, so there the total is that page's: Server Processing says what to do instead.
Which columns offer summaries is controlled per column: summable: true opts a column in, summable: false opts it out. When the flag is unset,
columns with dataType: 'number' are summable
automatically. storage sits beside defaults in the same object (prefs={{ storage: 'employees', defaults: { summaries } }}), and then summary choices survive reloads alongside column visibility and column order.
What the reader last chose wins over your defaults, an empty choice included: What the table remembers has the rest of the channel.
A summary outlives its column being hidden, and where the figure can go depends on the
layout. On the desktop table each summary sits in a cell under its own column, so hiding the
column takes the figure with it. In the card layout (a container narrower than cardsBelow) the summaries are a band of label and
value rows instead, so there the total stays on screen, under the column's title. The
aggregation keeps running either way, and the desktop cell comes back with its column.
Header Menu
Every column header exposes a ⋮ menu (visible on hover
and keyboard focus) that bundles the per-column actions, with no SmartFilterBar required. The
same column flags decide what each entry offers and what the toolbar's tool of that name offers:
| Entry | Shown when |
|---|---|
| Sort ascending / descending | a data column, unless sortable: false |
| Remove filter | a filter on this column is active |
| Group by column / Remove grouping | groupable: true, or sortable: true when groupable is unset (never while virtualized) |
| Summary → Sum / Average / Count / Minimum / Maximum / None | summable: true, or dataType: 'number' when summable is unset |
| Hide column | enableColumnVisibility and hideable ≠ false |
| Show "Salary" | one entry per currently hidden column, named after it |
Try It
Name | Department | Salary |
|---|---|---|
Emma Wilson | Platform | 142000 |
Liam Chen | Design | 98000 |
Sofia Martinez | Platform | 165000 |
James Park | Product | 92000 |
Aisha Patel | Data | 128000 |
const columns: Column<Employee>[] = [
{ accessor: 'name', title: 'Name', sortable: true },
{ accessor: 'department', title: 'Department', sortable: true, groupable: true },
{
accessor: 'salary',
title: 'Salary',
sortable: true,
summable: true,
dataType: 'number',
align: 'right'
}
];Hiding and restoring columns is covered in Column Configuration → Column Visibility.