Table
StableSemantic HTML tables for structured data, with tabular numerals, row states and the parts to build sortable, selectable data tables.
Anatomy
- 1ContainerScrolls the table sideways when it's wider than the page, instead of squeezing the columns.
- 2CaptionNames the table for screen readers. Show it below the table, or hide it visually.
- 3Header cellA column label in small, muted text. Sortable columns put a button here and set aria-sort.
- 4Numeric cellNumbers align right in tabular figures, so digits line up down the column.
- 5Row48px tall in the body, divided by hairlines. Body rows tint on hover, and with the brand color when selected.
Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/table.jsonUsage
import {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"<Table>
<TableCaption>Usage for September 2026.</TableCaption>
<TableHeader>
<TableRow>
<TableHead scope="col">Resource</TableHead>
<TableHead scope="col" className="text-right">Cost</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Compute</TableCell>
<TableCell className="text-right">$186.20</TableCell>
</TableRow>
</TableBody>
</Table>Table wraps the <table> in a horizontally scrolling container, so wide tables never break the page layout on small screens. Numbers use tabular figures by default, so digits line up column by column.
Examples
Caption and footer
A caption names the table for everyone — it's the first thing a screen reader announces. TableFooter holds totals on a tinted row.
Data table
Selection, sorting and row actions are composition, not configuration: a checkbox column, header buttons that cycle the sort, and a menu per row. The header checkbox shows an indeterminate state when some — but not all — rows are selected, and selected rows take the brand tint.
1 of 6 selected
<TableHead aria-sort={sort?.key === "amount" ? sort.direction : "none"}>
<Button variant="ghost" size="xs" onClick={() => toggleSort("amount")}>
Amount
<ArrowUpDownIcon data-icon="inline-end" />
</Button>
</TableHead>Empty
When a query returns nothing, keep the header so people understand what would be there, and put an Empty state in a single full-width cell with a way out.
Loading
Show skeleton rows in the shape of the real content while data loads, and mark the table aria-busy so assistive technology waits for it.
Guidelines
When to use
- To compare many objects across the same set of attributes.
- When people need to scan, sort, filter or act on rows.
When not to use
- For layout. Tables are for data; use grid or flex for arranging things on a page.
- For a handful of objects with rich content — use Cards or a list of Items.
- For a single record's fields — use a description list.
Alignment
- Text left, numbers right. Right-aligned numbers put units, tens and hundreds in the same column — together with tabular figures, magnitudes compare at a glance.
- Align the header with its column's content, including the sort button.
- Keep units in the header (Cost (USD), Duration (s)) rather than repeating them in every cell.
Density
Rows are 48px tall — comfortable for mixed content with badges and actions. For read-only, numbers-only tables, reduce cell height with className="h-10"; don't go below 32px, which is the smallest comfortable pointer target.
Row actions
Put actions for a single row in an overflow menu at the end of the row, and actions for many rows in a toolbar that appears when rows are selected. Don't make the whole row clickable and put buttons inside it.
Accessibility
prfct tables are real <table> elements, so rows, columns and headers are announced without extra ARIA.
| Key | Behavior |
|---|---|
Tab | Moves between interactive elements inside the table: checkboxes, sort buttons, row menus. |
Space | Toggles the focused row checkbox. |
EnterSpace | Activates the focused sort button or opens the row menu. |
- Name the table. Use
TableCaption, oraria-labelwhen a visible heading already names it. - Scope headers. Add
scope="col"to column headers (andscope="row"to row headers) so each cell is announced with its header. - Announce sorting. Set
aria-sorton the sortedTableHead(ascending,descendingornone). The icon alone isn't announced. - Label every control. Row checkboxes and menus need labels that name the row — "Select payment from Northwind Traders", not "Select".
- Selection isn't color. The selected tint is reinforced by the checked checkbox; don't rely on the tint alone.
API reference
Each part renders the matching HTML element and accepts all of its props.