A native <table> with the design system's borders/spacing — sorting
(TableHeaderCell), zebra rows, and row selection
(TableRow) are opt-in per-piece rather than table-wide
flags, since a given table only needs some of them.
tsx
| Name | Status |
|---|---|
| Ada Lovelace | Active |
| Grace Hopper | Inactive |
<Table zebra>
<TableHead>
<tr>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
</tr>
</TableHead>
<TableBody>
<TableRow>
<TableCell>Ada Lovelace</TableCell>
<TableCell>Active</TableCell>
</TableRow>
</TableBody>
</Table>tsx
| Name | Status |
|---|---|
| Ada Lovelace | Active |
| Alan Turing | Invited |
<Table>
...
</Table>Examples
Sorting is driven entirely by TableHeaderCell's own sortDirection/onSort — Table itself
doesn't know or care that any column is sortable:
tsx
| Name |
|---|
| Ada Lovelace |
| Grace Hopper |
const [sort, setSort] = useState<"ascending" | "descending">("ascending");
<Table>
<TableHead>
<tr>
<TableHeaderCell
sortDirection={sort}
onSort={() => setSort(sort === "ascending" ? "descending" : "ascending")}
>
Name
</TableHeaderCell>
</tr>
</TableHead>
<TableBody>{/* sorted rows */}</TableBody>
</Table>;For no results — a filtered table that matched nothing — reach for
EmptyState inside the table body rather than rendering an
empty <TableBody>.