Group row
In a grouped View, each group opens with a full-width row naming the field, the value and how many records it holds.
Playground
Change the controls to try each prop. The preview puts the row in a one-column table; the code lists only the props you changed.
| Stage: CV sent2 |
import { GroupRow } from '@/ui/group-row';<GroupRow field="Stage" value="CV sent" />Grouped by stage
Give the table groupOf and draw each group with groupRow. The row spans every column and holds no checkbox. Records with no value collect under Empty.
| Candidate | Company | Stage | Updated | |
|---|---|---|---|---|
| Stage: CV sent2 | ||||
| Maya Chen | Halcyon Bank | CV sent | 2h ago | |
| Tom Lindqvist | Orbital Freight | CV sent | Yesterday | |
| Stage: Placed1 | ||||
| Daniel Okafor | Halcyon Bank | Placed | 3 Oct | |
| Stage: Empty1 | ||||
| Elena Hart | Northbridge Talent | 28 Sep | ||
import { Lozenge } from '@/ui/lozenge';
import { GroupRow } from '@/ui/group-row';
import { Table, type TableColumn } from '@/ui/table';
type Candidate = { id: string; name: string; company: string; stage: string | null; updated: string };
const candidates: Candidate[] = [
{ id: 'c1', name: 'Maya Chen', company: 'Halcyon Bank', stage: 'CV sent', updated: '2h ago' },
{ id: 'c2', name: 'Tom Lindqvist', company: 'Orbital Freight', stage: 'CV sent', updated: 'Yesterday' },
{ id: 'c3', name: 'Daniel Okafor', company: 'Halcyon Bank', stage: 'Placed', updated: '3 Oct' },
{ id: 'c4', name: 'Elena Hart', company: 'Northbridge Talent', stage: null, updated: '28 Sep' },
];
const columns: TableColumn<Candidate>[] = [
{ key: 'name', label: 'Candidate', cell: (row) => row.name },
{ key: 'company', label: 'Company', cell: (row) => row.company },
{ key: 'stage', label: 'Stage', cell: (row) => (row.stage ? <Lozenge phase={row.stage === 'Placed' ? 'placed' : 'client'}>{row.stage}</Lozenge> : null) },
{ key: 'updated', label: 'Updated', cell: (row) => row.updated },
];
export default function GroupRowGroupedByStage() {
return (
<Table
label="Candidates"
columns={columns}
rows={candidates}
rowLabel={(row) => row.name}
selectable
groupOf={(row) => row.stage ?? ''}
groupRow={(group) => <GroupRow field="Stage" value={group.key || null} count={group.rows.length} span={group.span} />}
/>
);
}Collapsible
Pass open and onOpenChange from the table's group to add a caret: the row becomes a toggle, and the rows that open fade in. Without them the row is a plain heading, as in the app's record grid.
| Contact | Job title |
|---|---|
import { GroupRow } from '@/ui/group-row';
import { Table, type TableColumn } from '@/ui/table';
type Contact = { id: string; name: string; owner: string; title: string };
const contacts: Contact[] = [
{ id: 'p1', name: 'Hannah Moss', owner: 'Joseph Ariel', title: 'Head of talent' },
{ id: 'p2', name: 'Kwame Asante', owner: 'Joseph Ariel', title: 'Chief risk officer' },
{ id: 'p3', name: 'Lena Fischer', owner: 'Sara Lee', title: 'People partner' },
];
const columns: TableColumn<Contact>[] = [
{ key: 'name', label: 'Contact', cell: (row) => row.name },
{ key: 'title', label: 'Job title', width: 200, cell: (row) => row.title },
];
export default function GroupRowCollapsible() {
return (
<Table
label="Contacts"
columns={columns}
rows={contacts}
groupOf={(row) => row.owner}
defaultCollapsedGroups={['Joseph Ariel', 'Sara Lee']}
groupRow={(group) => (
<GroupRow field="Owner" value={group.key} count={group.rows.length} span={group.span} open={group.open} onOpenChange={group.onOpenChange} />
)}
/>
);
}Values by field type
The value reads as the cell would label it: a member's name, Yes or No, the option label, never an id. A group that carries over from the last page says "continued". A long value ends in an ellipsis; the count keeps its place.
| Owner: Joseph Ariel6 |
| Open to relocation: Yes4 |
| Seniority: Senior9 |
| Stage: Interview12continued |
| Current company: Empty3 |
| Skills: Python, Spark, Airflow, dbt, SQL, Kafka, Terraform and Kubernetes1,234,567 |
import { GroupRow } from '@/ui/group-row';
export default function GroupRowValues() {
return (
<table className="w-full max-w-120 table-fixed border-separate border-spacing-0 bg-surface">
<tbody>
<GroupRow field="Owner" value="Joseph Ariel" count={6} span={1} />
<GroupRow field="Open to relocation" value="Yes" count={4} span={1} />
<GroupRow field="Seniority" value="Senior" count={9} span={1} />
<GroupRow field="Stage" value="Interview" count={12} span={1} continued />
<GroupRow field="Current company" value={null} count={3} span={1} />
<GroupRow field="Skills" value="Python, Spark, Airflow, dbt, SQL, Kafka, Terraform and Kubernetes" count={1234567} span={1} />
</tbody>
</table>
);
}Draft row
The last row of every grid. "+ New candidate" turns it into a row you type into, cell by cell, as in a spreadsheet. Enter saves it; Escape cancels it.
Kanban board
A job's candidates as columns by stage, from CV sent to Placed. Drag a card to move it, or use its Move to menu. Earlier stages fold into one column.