Row Expansion
Row expansion enables tables to display hierarchical data by allowing rows to expand and reveal nested sub-rows. This is useful for representing parent-child relationships, grouped data, or tree structures within a table.
The demo below shows a table with expandable rows containing nested data. Each row with children displays an expand button that toggles visibility of its sub-rows. The header includes an expand-all button that expands or collapses all rows at once. Visual indentation indicates the depth level of each row in the hierarchy.
Username | Account Status | Role | Avg Session Duration | Company Name | Last Visited At | Visit Count |
|---|
{
"expanded": {},
"rowSelection": {}
}
import {useMemo, useState} from "react"
import {ChevronDown, ChevronRight} from "lucide-react"
import {
type ColumnDef,
type ExpandedState,
getCoreRowModel,
getExpandedRowModel,
getPaginationRowModel,
} from "@qualcomm-ui/core/table"
import {Button} from "@qualcomm-ui/react/button"
import {Checkbox} from "@qualcomm-ui/react/checkbox"
import {Pagination} from "@qualcomm-ui/react/pagination"
import {ProgressRing} from "@qualcomm-ui/react/progress-ring"
import {
flexRender,
Table,
useReactTable,
useTablePagination,
} from "@qualcomm-ui/react/table"
import {CodeHighlight} from "@qualcomm-ui/react-mdx/code-highlight"
import {type User, useUserData} from "./use-data"
export function RowExpansionDemo() {
const {data = [], isFetching, refetch} = useUserData(100, 5, 3)
// always memoize your data and columns
const columns: ColumnDef<User>[] = useMemo(
() => [
{
accessorKey: "username",
cell: ({getValue, row}) => {
const indeterminate = row.getIsSomeSelected()
const checked = row.getIsSelected()
return (
<div
className="inline-flex h-full items-center gap-2"
style={{
// Since rows are flattened by default,
// we can use the row.depth property
// and paddingLeft to visually indicate the depth
// of the row
paddingLeft: `${row.depth * 2}rem`,
}}
>
<>
<Checkbox
checked={checked}
indeterminate={indeterminate}
onCheckedChange={(nextState) => row.toggleSelected(nextState)}
size="sm"
/>
{row.getCanExpand() ? (
<div className="inline-flex items-center justify-center">
<Table.RowExpandButton row={row} />
</div>
) : null}
<span>{getValue() as string}</span>
</>
</div>
)
},
header: ({table}) => {
return (
<>
<div className="flex items-center gap-2">
<Table.ColumnHeaderAction
aria-label="Expand all table rows"
icon={
table.getIsAllRowsExpanded() ? ChevronDown : ChevronRight
}
onClick={table.getToggleAllRowsExpandedHandler()}
/>
<span>Username</span>
</div>
</>
)
},
id: "username",
},
{
accessorKey: "accountStatus",
header: "Account Status",
id: "accountStatus",
},
{
accessorKey: "role",
header: "Role",
id: "role",
},
{
accessorKey: "averageSessionDuration",
header: "Avg Session Duration",
id: "averageSessionDuration",
},
{
accessorKey: "companyName",
header: "Company Name",
id: "companyName",
minSize: 200,
},
{
accessorKey: "lastVisitedAt",
header: "Last Visited At",
id: "lastVisitedAt",
minSize: 205,
},
{
accessorKey: "visitCount",
header: "Visit Count",
id: "visitCount",
},
],
[],
)
const refreshData = () => refetch()
const [expanded, setExpanded] = useState<ExpandedState>({})
const table = useReactTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSubRows: (row) => row.subRows,
onExpandedChange: setExpanded,
state: {
expanded,
},
})
const paginationProps = useTablePagination(table)
return (
<div className="flex w-full flex-col gap-4 p-2">
<Table.Root>
<Table.ActionBar>
<Button onClick={() => void refreshData()} variant="outline">
Refresh Data
</Button>
{isFetching ? <ProgressRing size="xs" /> : null}
</Table.ActionBar>
<Table.ScrollContainer>
<Table.Table>
<Table.Header>
{table.getHeaderGroups().map((headerGroup) => (
<Table.Row key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<Table.HeaderCell
key={header.id}
colSpan={header.colSpan}
>
{header.isPlaceholder ? null : (
<div className="inline-flex flex-col gap-1">
<div
className="inline-flex items-center justify-center"
style={{
minHeight: header.column.getCanFilter()
? 28
: "auto",
}}
>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</div>
</div>
)}
</Table.HeaderCell>
)
})}
</Table.Row>
))}
</Table.Header>
<Table.Body>
{table.getRowModel().rows.map((row) => {
return (
<Table.Row key={row.id} isSelected={row.getIsSelected()}>
{row.getVisibleCells().map((cell) => {
return (
<Table.Cell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</Table.Cell>
)
})}
</Table.Row>
)
})}
</Table.Body>
</Table.Table>
</Table.ScrollContainer>
<Table.Pagination {...paginationProps}>
<Pagination.PageMetadata>
{({count, pageEnd, pageStart}) => (
<>
{pageStart}-{pageEnd} of {count} results
</>
)}
</Pagination.PageMetadata>
<Pagination.PageButtons />
</Table.Pagination>
</Table.Root>
<CodeHighlight
className="max-h-[400px] overflow-y-auto"
code={JSON.stringify(
{expanded, rowSelection: table.getState().rowSelection},
null,
2,
)}
language="json"
/>
</div>
)
}