ComponentsTables and views

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
States
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.

CandidateCompanyStageUpdated
Stage: CV sent2
Maya ChenHalcyon BankCV sent2h ago
Tom LindqvistOrbital FreightCV sentYesterday
Stage: Placed1
Daniel OkaforHalcyon BankPlaced3 Oct
Stage: Empty1
Elena HartNorthbridge Talent28 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.

ContactJob 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>
  );
}

On this page