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

PartRole
Table.RootOwns selectionMode, disabledKeys, sortDescriptor.
Table.HeaderThe header row.
Table.ColumnOne column. isRowHeader marks the column that names each row.
Table.BodyThe rows. Takes items plus a render function, or static rows.
Table.RowOne row.
Table.CellOne 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.

NameTypeSize
DocumentsFolder—
quarterly-report.pdfPDF1.2 MB
notes.txtText file4 KB
team-photo.pngImage3.8 MB
budget.xlsxSpreadsheet88 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

NameType
DocumentsFolder
quarterly-report.pdfPDF
notes.txtText file
team-photo.pngImage
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.

NameTypeSize
budget.xlsxSpreadsheet88 KB
DocumentsFolder—
notes.txtText file4 KB
quarterly-report.pdfPDF1.2 MB
team-photo.pngImage3.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

NameType
DocumentsFolder
quarterly-report.pdfPDF
notes.txtText file
team-photo.pngImage
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

NameType
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>
  )
}