Table
Data grid built on React Aria, with keyboard navigation, row selection, and column sorting.
import { Table } from "@pockets-ui/components"For lists whose rows are a single unit of content rather than columns of comparable data, use GridList.
Anatomy
| Part | Role |
|---|---|
Table.Root | Owns selectionMode, disabledKeys, sortDescriptor. |
Table.Header | The header row. |
Table.Column | One column. isRowHeader marks the column that names each row. |
Table.Body | The rows. Takes items plus a render function, or static rows. |
Table.Row | One row. |
Table.Cell | One cell, in column order. |
Give the root an aria-label (or aria-labelledby) and mark exactly one column
isRowHeader — that's what a screen reader announces to identify a row as the
user moves down the table.
Basic
Pass items to Table.Body with a render function instead of mapping yourself.
React Aria reads each item's id as its key, which is what selection and
disabledKeys refer to.
| Name | Type | Size |
|---|---|---|
| Documents | Folder | — |
| quarterly-report.pdf | 1.2 MB | |
| notes.txt | Text file | 4 KB |
| team-photo.png | Image | 3.8 MB |
| budget.xlsx | Spreadsheet | 88 KB |
Code
import { Table } from "@pockets-ui/components"
const files = [
{ id: 1, name: "Documents", type: "Folder", size: "—" },
{ id: 2, name: "quarterly-report.pdf", type: "PDF", size: "1.2 MB" },
{ id: 3, name: "notes.txt", type: "Text file", size: "4 KB" },
{ id: 4, name: "team-photo.png", type: "Image", size: "3.8 MB" },
{ id: 5, name: "budget.xlsx", type: "Spreadsheet", size: "88 KB" },
]
export default function TableBasic() {
return (
<Table.Root aria-label="Files">
<Table.Header>
<Table.Column id="name" isRowHeader>
Name
</Table.Column>
<Table.Column id="type">Type</Table.Column>
<Table.Column id="size">Size</Table.Column>
</Table.Header>
<Table.Body items={files}>
{(file) => (
<Table.Row>
<Table.Cell>{file.name}</Table.Cell>
<Table.Cell>{file.type}</Table.Cell>
<Table.Cell>{file.size}</Table.Cell>
</Table.Row>
)}
</Table.Body>
</Table.Root>
)
}Selection
| Name | Type | |
|---|---|---|
| Documents | Folder | |
| quarterly-report.pdf | ||
| notes.txt | Text file | |
| team-photo.png | Image |
Code
import { Table } from "@pockets-ui/components"
const files = [
{ id: 1, name: "Documents", type: "Folder" },
{ id: 2, name: "quarterly-report.pdf", type: "PDF" },
{ id: 3, name: "notes.txt", type: "Text file" },
{ id: 4, name: "team-photo.png", type: "Image" },
]
export default function TableSelection() {
return (
// selectionMode on the root makes Header/Row inject a checkbox column.
<Table.Root
aria-label="Files (selectable)"
selectionMode="multiple"
defaultSelectedKeys={[2]}
>
<Table.Header>
<Table.Column id="name" isRowHeader>
Name
</Table.Column>
<Table.Column id="type">Type</Table.Column>
</Table.Header>
<Table.Body items={files}>
{(file) => (
<Table.Row>
<Table.Cell>{file.name}</Table.Cell>
<Table.Cell>{file.type}</Table.Cell>
</Table.Row>
)}
</Table.Body>
</Table.Root>
)
}Sorting
Mark sortable columns with allowsSorting, then keep sortDescriptor in state.
The table reports which column and direction the user asked for; sorting the
rows is your job — it never reorders data behind your back.
| Name | Type | Size |
|---|---|---|
| budget.xlsx | Spreadsheet | 88 KB |
| Documents | Folder | — |
| notes.txt | Text file | 4 KB |
| quarterly-report.pdf | 1.2 MB | |
| team-photo.png | Image | 3.8 MB |
Code
import { type SortDescriptor, Table } from "@pockets-ui/components"
import * as React from "react"
const files = [
{ id: 1, name: "Documents", type: "Folder", size: "—" },
{ id: 2, name: "quarterly-report.pdf", type: "PDF", size: "1.2 MB" },
{ id: 3, name: "notes.txt", type: "Text file", size: "4 KB" },
{ id: 4, name: "team-photo.png", type: "Image", size: "3.8 MB" },
{ id: 5, name: "budget.xlsx", type: "Spreadsheet", size: "88 KB" },
]
export default function TableSorting() {
const [sortDescriptor, setSortDescriptor] = React.useState<SortDescriptor>({
column: "name",
direction: "ascending",
})
// The table reports what the user asked for; sorting the rows is up to you.
const sorted = React.useMemo(() => {
const key = sortDescriptor.column as keyof (typeof files)[number]
const items = [...files].sort((a, b) =>
String(a[key]).localeCompare(String(b[key])),
)
return sortDescriptor.direction === "descending" ? items.reverse() : items
}, [sortDescriptor])
return (
<Table.Root
aria-label="Files (sortable)"
sortDescriptor={sortDescriptor}
onSortChange={setSortDescriptor}
>
<Table.Header>
<Table.Column id="name" isRowHeader allowsSorting>
Name
</Table.Column>
<Table.Column id="type" allowsSorting>
Type
</Table.Column>
<Table.Column id="size">Size</Table.Column>
</Table.Header>
<Table.Body items={sorted}>
{(file) => (
<Table.Row>
<Table.Cell>{file.name}</Table.Cell>
<Table.Cell>{file.type}</Table.Cell>
<Table.Cell>{file.size}</Table.Cell>
</Table.Row>
)}
</Table.Body>
</Table.Root>
)
}Disabled rows
| Name | Type | |
|---|---|---|
| Documents | Folder | |
| quarterly-report.pdf | ||
| notes.txt | Text file | |
| team-photo.png | Image |
Code
import { Table } from "@pockets-ui/components"
const files = [
{ id: 1, name: "Documents", type: "Folder" },
{ id: 2, name: "quarterly-report.pdf", type: "PDF" },
{ id: 3, name: "notes.txt", type: "Text file" },
{ id: 4, name: "team-photo.png", type: "Image" },
]
export default function TableDisabledRows() {
return (
<Table.Root
aria-label="Files (disabled rows)"
selectionMode="multiple"
disabledKeys={[1, 4]}
>
<Table.Header>
<Table.Column id="name" isRowHeader>
Name
</Table.Column>
<Table.Column id="type">Type</Table.Column>
</Table.Header>
<Table.Body items={files}>
{(file) => (
<Table.Row>
<Table.Cell>{file.name}</Table.Cell>
<Table.Cell>{file.type}</Table.Cell>
</Table.Row>
)}
</Table.Body>
</Table.Root>
)
}Empty state
| Name | Type |
|---|---|
| No results found. | |
Code
import { Table } from "@pockets-ui/components"
export default function TableEmptyState() {
return (
<Table.Root aria-label="Search results">
<Table.Header>
<Table.Column isRowHeader>Name</Table.Column>
<Table.Column>Type</Table.Column>
</Table.Header>
<Table.Body renderEmptyState={() => "No results found."}>{[]}</Table.Body>
</Table.Root>
)
}