# Tables and collections

Present records with sorting, filtering, selection, and useful empty states.

## Choose the interaction model

Use [Table](/docs/components/table/) when records have comparable columns. Use [ResourceList](/docs/components/resource-list/) for flexible application rows and [ListBox](/docs/components/list-box/) for choosing one item from a visible list. ResourceList has no built-in selection model: `ResourceListItem` only reflects the `selected` and `interactive` states your application sets.

## Sorting and selection

Select table rows with checkboxes

[Complete TSX file](/examples/table/checkbox-selection.tsx)

```tsx
"use client";

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
  TableSelectionCheckbox,
} from "@/siteplane/components/table";
import { useState } from "react";
import type { Selection } from "react-aria-components";

const projects = [
  {
    id: "website-redesign",
    project: "Website Redesign",
    role: "Owner",
    team: "Frontend Team",
    budget: "$12,500",
  },
  {
    id: "mobile-app",
    project: "Mobile App",
    role: "Editor",
    team: "Mobile Team",
    budget: "$8,750",
  },
  {
    id: "api-integration",
    project: "API Integration",
    role: "Editor",
    team: "Backend Team",
    budget: "$5,200",
  },
];

export default function Example() {
  const [selectedKeys, setSelectedKeys] = useState<Selection>(
    new Set(["website-redesign"]),
  );
  return (
    <Table
      aria-label="Projects"
      selectionMode="multiple"
      selectionTrigger="checkbox"
      selectedKeys={selectedKeys}
      onSelectionChange={setSelectedKeys}
    >
      <TableHeader>
        <TableHead id="selection">
          <TableSelectionCheckbox />
        </TableHead>
        <TableHead id="project" isRowHeader>
          Project
        </TableHead>
        <TableHead id="role">Role</TableHead>
        <TableHead id="team">Team</TableHead>
        <TableHead id="budget" align="end">
          Budget
        </TableHead>
      </TableHeader>
      <TableBody>
        {projects.map((project) => (
          <TableRow
            key={project.id}
            id={project.id}
            textValue={project.project}
          >
            <TableCell>
              <TableSelectionCheckbox />
            </TableCell>
            <TableCell>{project.project}</TableCell>
            <TableCell>{project.role}</TableCell>
            <TableCell>{project.team}</TableCell>
            <TableCell align="end">{project.budget}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}
```

Table already exposes sorting and selection. Supply stable row ids, row textValue, a row-header column, and the correct state callbacks. A sort descriptor describes the requested order; sort the actual application data before rendering it. For controlled selection, account for both a Set of keys and `"all"`.

## Filtering

[FilterBar](/docs/components/filter-bar/) supplies layout slots, not query execution. Connect Input, Select or other controls to application-owned state, derive visible rows, and expose a clear result count. Keep sort and selection stable when the filter changes, and decide whether bulk actions affect all records or only visible ones.

## Loading, empty, and failure

Use `TableBody` `renderEmptyState` for an empty table. Table has no loading prop: while data loads, show a [Skeleton](/docs/components/skeleton/) and set `aria-busy` on the results region. For a richer empty screen, compose an [EmptyState](/docs/components/empty-state/). “No records yet” differs from “No matches”; offer the relevant next action. Keep service errors visible instead of disguising them as an empty result.

On small screens, preserve column meaning and horizontal scrolling. Do not shrink all text or remove row actions to force a wide table into the viewport. Test interactive controls inside rows with the keyboard.

## Complete application

[Resource management](/docs/examples/resource-management/) includes local data, text and status filters, sortable columns, empty feedback, and a details/confirmation flow. Copy its full file after installation. Replace local data with your service; pagination, authorization, and server query semantics remain application responsibilities.
