Data tables
Density, alignment, sorting, filtering, selection and bulk actions — tables that stay readable at a hundred rows.
Tables are for comparing. Every decision below serves one goal: let people scan a column, compare rows and act on them without losing their place.
examples/patterns/data-table.tsx
6 of 6 invoices
Alignment
- Text left, numbers right. Right-aligned numbers with tabular figures line up digit by digit. prfct tables use tabular figures by default.
- Headers align with their content.
- Units in the header, not repeated in every cell, when a column shares one.
Density
Rows are 48px by default — comfortable for mixed content. For dense operational tables, use 40px rows with sm controls; don't go below 32px. Keep one density per table.
Sorting and filtering
- Make sortable headers buttons, with an icon that shows the direction, and set
aria-sorton the header cell. - Put search and filters above the table, and show the result count below it.
- When filters exclude everything, keep the headers and show an empty state inside the table with a way to clear filters.
Selection and bulk actions
- A checkbox column with a header checkbox that is indeterminate when some — but not all — rows are selected.
- When rows are selected, a bulk action bar replaces the toolbar, states the count and offers the actions. Destructive bulk actions follow Destructive actions.
- Selected rows use
data-state="selected", tinted withbrand-2.
Pagination
Use pagination when position matters — people need to find the invoice from last March again. Use infinite scroll only for feeds that are browsed, not searched. Always show the total.
Accessibility
- Use real
tablemarkup:thead,thwithscope, and acaption(visually hidden if needed). - Every row checkbox has a label that names the row: Select INV-2043.
- Row actions are reachable with the keyboard; avoid actions that only appear on hover.