Skip to content

Table

Stable

Semantic HTML tables for structured data, with tabular numerals, row states and the parts to build sortable, selectable data tables.

DeployStatusTime
#1284Building31s
#1283Ready48s
#1282Error12s

Anatomy

Recent deployments
DeployBranchDuration
#1284main31s
#1283fix/focus1m 48s
  1. 1ContainerScrolls the table sideways when it's wider than the page, instead of squeezing the columns.
  2. 2CaptionNames the table for screen readers. Show it below the table, or hide it visually.
  3. 3Header cellA column label in small, muted text. Sortable columns put a button here and set aria-sort.
  4. 4Numeric cellNumbers align right in tabular figures, so digits line up down the column.
  5. 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.json

Usage

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

A caption names the table for everyone — it's the first thing a screen reader announces. TableFooter holds totals on a tinted row.

Usage for September 2026. Prices exclude tax.
ResourceUnitQuantityCost
ComputeGB-hours12,480$186.20
BandwidthGB3,902$58.53
StorageGB-months812$16.24
Build minutesminutes6,140$30.70
Total$291.67

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

StatusActions
Kestrel Healthaccounts@kestrel.health
Paid$9,750.00
Northwind Tradersbilling@northwind.dev
Paid$4,820.00
Pinecone Labsbilling@pinecone.dev
Paid$2,190.75
Lumen Studiofinance@lumen.studio
Pending$1,260.00
Oakline Banktreasury@oakline.com
Refunded$640.00
Atlas Roboticsap@atlasrobotics.io
Failed$312.50
<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>
Large datasets
The example keeps state in React. For server-side sorting, pagination and virtualization, pair these parts with a headless library such as TanStack Table — the markup stays the same.

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.

DeploymentBranchAuthorDuration
No deployments match “hotfix/*”

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.

CustomerPlanMRR
Loading customers…

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.
PlanMRR
Pro$1,240.00
Team$86.50
Enterprise$12,900.00
Do.Numbers right-aligned with tabular figures: magnitudes line up.
PlanMRR
Pro$1,240.00
Team$86.50
Enterprise$12,900.00
Don’t.Left-aligned numbers hide their magnitude until you read every digit.

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.

KeyBehavior
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, or aria-label when a visible heading already names it.
  • Scope headers. Add scope="col" to column headers (and scope="row" to row headers) so each cell is announced with its header.
  • Announce sorting. Set aria-sort on the sorted TableHead (ascending, descending or none). 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.

ComponentElementNotes
Table<table>Wrapped in a horizontally scrolling <div data-slot="table-container">. Tabular numerals on.
TableHeader<thead>Bottom border; header rows don't take a hover state.
TableBody<tbody>Removes the border from the last row.
TableFooter<tfoot>Tinted surface and medium weight for totals.
TableRow<tr>Hover tint; data-state="selected" applies the selected tint.
TableHead<th>40px tall, small muted text. Checkbox columns shrink to fit.
TableCell<td>48px tall. First and last cells get 16px outer padding.
TableCaption<caption>Rendered below the table in muted text.