Skip to content

React table column groups — spanning headers, collapsible

See it working: collapse column groups in Mantine — one table, three groups, open by default: Contact folds to a chevron, Assignment keeps Team, Delivery shows a money-for-days brief (align: "start"). Actions stays ungrouped at the end. The same page exists for MUI, Chakra, antd, Radix, Base UI, shadcn and Tailwind.

A parent with children is a column group: one spanning header over its leaves. Collapse is per group — each parent decides what remains. Omit collapsibleColumnGroups and the headers stay static.

This is not row grouping. Rows fold under a groupBy; columns span under a parent header.

Related: Columns · Column management

columns accepts a mix of leaf ColumnDefs and ColumnGroupDef parents (ColumnInput). Tree groups default marryChildren: true, so a reorder cannot split them.

import { DataTable, type ColumnInput } from "@adapttable/mantine";
const columns: ColumnInput<Person>[] = [
{
header: "Contact",
children: [
{ key: "name", header: "Name" },
{ key: "role", header: "Role" },
],
},
{
header: "Assignment",
collapsedKey: "team",
children: [
{ key: "team", header: "Team" },
{ key: "status", header: "Status" },
],
},
{
header: "Delivery",
align: "start",
collapsedRender: (row) => `${row.budget} for 35 days`,
children: [
{ key: "timeline", header: "Timeline" },
{ key: "budget", header: "Budget" },
],
},
];
<DataTable
data={rows}
columns={columns}
rowKey={(row) => row.id}
collapsibleColumnGroups
/>;

HTML-table kits (Mantine, MUI, Chakra, Radix, Base UI, unstyled) render the same tree with rowSpan on ungrouped leaves, so Actions sits beside Delivery and its children instead of under a blank group row. Ant uses native grouped columns; htmlGroupedHeaderPlan is that tree flattened for <th>. HTML kits draw the line under an open group with groupedHeaderChildRule so adjacent groups do not share one stroke.

Pass collapsibleColumnGroups and each real group header gains a toggle. What a collapsed group shows is that group’s own options — there is no table-wide mode.

Result How
Thin arrow stub (chevron + hairline cell) No collapsedKey, no collapsedRender, no child groupShow: "closed"
Keep one child collapsedKey: "timeline" (or that leaf groupShow: "closed")
Custom one cell collapsedRender: (row) => … (wins over collapsedKey)
Always visible child groupShow: "always"
Open-only child groupShow: "open" (default under a collapsible group)

A collapsed collapsedRender or stub group drops the child header row — the title fills the header height, the same way an ungrouped leaf does. collapsedKey keeps both rows: the group title and the stayed child’s header. Each group’s rule under the title covers only that group’s children; adjacent groups do not share one stroke.

The stub hides the visible caption (hideLabel). The toggle’s aria-label still names the group (Expand column group: Delivery). The stub column is locked to a chevron width so leftover table space cannot stretch it into a blank data column.

collapsedRender wins over collapsedKey. State lives on columnLayout.collapsedGroups and the URL (colGroupCollapse); group ids are path.join("\u001f") (columnGroupId) so a label may contain /.

Adjacent leaves with the same group still span under one header. A string is one level; a path stacks rows. That shortcut is presentational: a reorder that breaks adjacency splits the span, and collapse (when armed) is an arrow stub unless a child sets groupShow. Prefer a tree parent when the group has collapse options or must stay married.

const columns: ColumnDef<Person>[] = [
{ key: "firstName", header: "First", group: "Name" },
{ key: "lastName", header: "Last", group: "Name" },
{ key: "hiredAt", header: "Hired" },
];

The card layout has no header row. The same visible-column filter applies — cards hide the leaves a collapsed group hid on desktop. The arrow stub itself is hideOnMobile, so a collapsed Delivery does not become an empty card field.

flattenColumnTree turns a mixed ColumnInput[] into leaves plus a ColumnGroupRecord map. applyCollapsedColumnGroups hides leaves under collapsed ids and inserts a stub (COLUMN_GROUP_STUB_PREFIX, isColumnGroupStubKey) or a collapsedRender column (COLUMN_GROUP_RENDER_PREFIX, isColumnGroupRenderKey). marriedOrderHolds rejects a reorder that would split a married tree group. columnGroupHeaderCaption returns the visible caption, or null on a stub. htmlGroupedHeaderPlan is the HTML-table rowspan plan (ungrouped leaves beside the group band; a collapsed brief fills the header). groupedHeaderChildRule is the inset hairline under an open group title. groupedHeaderCellStyle applies that hairline and columnGroupStubStyle (the 36px lock so a stub cannot stretch). groupedHeaderLabelStyle keeps the collapse chevron on the same line as the group title so a one-child group cannot wrap the arrow above the caption. isColumnGroup narrows a ColumnInput to a parent.

See it live in the demo.