Data tables people can scan
Align numbers on the end, let people sort the columns they compare, and name every row so each cell makes sense on its own.
| Employee | Team | Days left |
|---|---|---|
Ahmed Raza | Engineering | 3 |
Omar Siddiqui | Sales | 8 |
Sara Khan | Design | 14 |
Ayesha Malik | Finance | 21 |
The problem
Tables are read in two ways: down a column to compare, and across a row to check one record. Centred numbers, unsortable columns, rows without a name and row actions hidden until hover make both slow. People export to a spreadsheet just to sort.
When to use it
Use it for
- Records with the same fields that people compare or act on: employees, invoices, requests.
- More than about five rows with three or more attributes each.
Not for
- A few items with one or two attributes: use a list of ListItem rows.
- Visual content like photos or profiles: use a grid of Cards.
- Narrow phone screens where each record needs its main value only: use a list that opens a detail page.
Anatomy
- ToolbarTableToolbar: search and filters at the start, table actions at the end.
- HeaderTableHeaderCell per column, with allowsSorting on the columns people compare.
- Row headerThe column that names the row, usually the person or item: isRowHeader.
- Number cellstype="number": end-aligned, tabular figures, so digits line up.
- Row actionsVisible IconButtons, each labelled with the action and the row's name.
- FooterTableFooter: the range, the rows-per-page select and the page controls.
How to build it
- Give the table an aria-label that says what the rows are, and make the name column the row header.
- Align text to the start and numbers to the end, with align="right" on numeric headers and type="number" cells.
- Add allowsSorting to the columns people compare, and sort by the most useful one by default.
- Keep row actions visible. One or two IconButtons, the rest in a menu.
- Show an EmptyState in the body when there are no rows, saying why.
- Put the table in a scroll region so it scrolls sideways on narrow screens, not the page.
app.tsx
import { Table, TableBody, TableCell, TableHeader, TableHeaderCell, TableRow } from "@nexera-ui/react";
import { useState, type ComponentProps } from "react";
type SortDescriptor = NonNullable<ComponentProps<typeof Table>["sortDescriptor"]>;
const [sort, setSort] = useState<SortDescriptor>({ column: "name", direction: "ascending" });
<Table aria-label="Leave balances" sortDescriptor={sort} onSortChange={setSort}>
<TableHeader>
<TableHeaderCell id="name" isRowHeader allowsSorting label="Employee" />
<TableHeaderCell id="team" allowsSorting label="Team" />
<TableHeaderCell id="days" allowsSorting align="right" label="Days left" />
</TableHeader>
<TableBody items={sortRows(rows, sort)}>
{(row) => (
<TableRow id={row.id}>
<TableCell type="avatar" name={row.name} email={row.email} />
<TableCell>{row.team}</TableCell>
<TableCell type="number" value={String(row.days)} />
</TableRow>
)}
</TableBody>
</Table>Accessibility
| Key | Action |
|---|---|
| Tab | Moves into the table, then on to the next control after it. |
| Arrow keys | Move between cells. Home and End go to the first and last cell of a row. |
| Enter / Space | Sorts by the focused column header. |
- Sortable headers carry aria-sort, so screen readers say “sorted ascending”.
- The row header column means each cell is read with the row's name, not just “14”.
- Announce the result after sorting or filtering politely: “Sorted by days left, descending”.
- Row action buttons are named with the row: “Edit Sara Khan”, not “Edit”.
Do and don't
DoEnd-align numbers and use tabular figures so the digits line up.
Don'tCentre numbers, or align them to the start.
DoLet people sort the columns they compare, and show which column is sorted.
Don'tMake people export to a spreadsheet to sort.
DoKeep row actions visible and labelled with the row's name.
Don'tShow actions only on hover, where keyboard and touch users can't find them.