Table
Styled semantic table primitives — a scrollable container plus header, body, footer, row, head, cell, and caption parts.
- Status
- Since
0.1.0- Accessibility pattern
- native table semantics
Last updated
| Invoice | Status | Method | Amount |
|---|---|---|---|
| INV-001 | Credit card | $250.00 | |
| INV-002 | PayPal | $150.00 | |
| INV-003 | Bank transfer | $350.00 | |
| INV-004 | Credit card | $90.00 |
Install
Add Table from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/tableThe same command installs the registry items it composes: @vegastack/use-overflow.
Usage
import {
Table,
TableHeader,
TableBody,
TableFooter,
TableRow,
TableHead,
TableCell,
TableCaption,
} from "@/components/ui/table";
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Role</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Ada</TableCell>
<TableCell>Engineer</TableCell>
</TableRow>
</TableBody>
</Table>;Anatomy
Table is a set of flat, server-safe primitives that map directly onto native table elements. The
root Table wraps a <table> in a horizontally scrollable container so wide tables never overflow
their parent. Every part forwards its ref and exposes a data-slot:
<Table>
<TableCaption>{/* describes the table */}</TableCaption>
<TableHeader>
<TableRow>
<TableHead>{/* column label */}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>{/* data */}</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell>{/* summary / totals */}</TableCell>
</TableRow>
</TableFooter>
</Table>Table— wraps a<table>in aTableScrollRegion(data-slot="table", container isdata-slot="table-container").w-full caption-bottom text-sm.TableHeader— the<thead>group (data-slot="table-header"). Adds a bottom border to each header row.TableBody— the<tbody>group (data-slot="table-body"). Drops the last row's border for a clean bottom edge.TableFooter— the<tfoot>group (data-slot="table-footer"). Muted background with a top border for totals.TableRow— a<tr>(data-slot="table-row"). Hover highlight anddata-selectedselected state. Rows insideTableHeaderdo not take the hover — a header is not a row you can act on.TableHead— a header cell<th>(data-slot="table-head"). Muted, start-aligned, medium-weight label.TableCell— a data cell<td>(data-slot="table-cell"). Vertically centered, and wrapping by default over the--table-cell-min-widthfloor.TableCaption— the<caption>(data-slot="table-caption"), rendered below the table.TableScrollRegion— the scroll viewport (data-slot="table-container"), the family's one client leaf. It measures itself and takes a tab stop only while it can actually scroll.
Examples
A caption, a selected row, and a footer totals row:
| Invoice | Status | Amount |
|---|---|---|
| INV-001 | $250.00 | |
| INV-002 | $150.00 | |
| INV-003 | $350.00 | |
| Total | $750.00 | |
Wrapping is the default
Body cells wrap (overflow-wrap: anywhere) over a per-cell minimum width, so one long value breaks
inside its own column instead of pushing the whole table sideways. Horizontal scrolling is reserved
for tables that are genuinely wide.
Two column shapes are unreadable when broken and opt back out with whitespace-nowrap: figures and
mono values (ids, refs, timestamps). DataList and DataGrid express the same rule declaratively —
column.nowrap, which defaults to true for align="end" and mono columns.
Retune the floor per table by overriding --table-cell-min-width (default calc(var(--spacing) * 20),
80px) in the table's className.
| Ref | Request | Credit |
|---|---|---|
| REQ-4821 | Storage quota exceeded on the production bucketThe nightly export wrote 42 GB of intermediate artefacts before the retention job ran, so the quota alarm fired at 03:14 UTC and paused ingestion for eleven minutes. | $1,240.00 |
| REQ-4822 | Webhook retries exhaustedSix consecutive deliveries to https://hooks.internal.example.com/v2/billing/settlement returned 504; the endpoint is now in cooldown until it answers a probe. | $86.00 |
Horizontal overflow, and reaching it by keyboard
When a table is genuinely wider than its parent, the data-slot="table-container" viewport
(overflow-x-auto) scrolls instead of overflowing — wide tables never break the surrounding layout.
A scrollable region that cannot be focused can only be scrolled with a pointer. So the viewport
measures itself and takes tabIndex={0} only while it actually scrolls: a table that fits adds
nothing to the tab order, and a table that does not is reachable with Tab and scrolled with
the arrow keys. Name it with scrollLabel (it falls back to the table's own aria-label) and the
viewport is exposed as role="region"; leave it unnamed and it stays a plain focusable container,
because an unnamed landmark is worse than none.
| Invoice | Status | Method | Customer | Issued | Due | Amount | |
|---|---|---|---|---|---|---|---|
| INV-001 | Paid | Credit card | Ada Lovelace | ada@analytical.dev | 2026-05-01 | 2026-05-15 | $250.00 |
| INV-002 | Pending | PayPal | Grace Hopper | grace@cobol.mil | 2026-05-03 | 2026-05-17 | $150.00 |
| INV-003 | Overdue | Bank transfer | Alan Turing | alan@enigma.uk | 2026-04-20 | 2026-05-04 | $350.00 |
Selectable rows
A leading checkbox column for row selection. The header checkbox toggles all rows (and shows the
indeterminate state for a partial selection), each selected TableRow carries data-selected (paired
with aria-selected) for the highlight, and the checkbox cells collapse their inline-end padding via the
[&:has([role=checkbox])]:pe-0 rule on TableHead/TableCell.
| Invoice | Status | Amount | |
|---|---|---|---|
| INV-001 | $250.00 | ||
| INV-002 | $150.00 | ||
| INV-003 | $350.00 | ||
| INV-004 | $90.00 |
Spreadsheet voice
Three Wave-2 props for data-heavy screens, all off by default: grid draws the full cell
grid (every cell's inline-end hairline), headerTone="ink" switches headers to the 14/500
foreground voice, and density="compact" tightens rows to ~32px. They compose freely — and
density/headerTone/grid flow to TableHead/TableCell via data-* flags, so the
family stays server-safe.
| Company | Owner | ARR |
|---|---|---|
| Globex | Ada Lovelace | $1.2M |
| Initech | Grace Hopper | $840K |
| Umbrella | Edsger Dijkstra | $310K |
API Reference
Table adds the three spreadsheet-voice props (grid, headerTone,
density), the scrollLabel that names the scroll viewport, and
containerProps — forwarded to the data-slot="table-container" viewport that
owns overflow-x-auto. Use containerProps to attach sticky headers, a
fixed-height viewport, or a virtualizer's scroll element; the <table> itself
cannot own a scroll viewport.
| Prop | Type | Default | Description |
|---|---|---|---|
containerProps | React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> | — | Props (including ref) forwarded to the scroll container element
(data-slot="table-container", which owns overflow-x-auto). This is the
attachment point for sticky headers, fixed-height viewports, and
virtualization — the <table> itself cannot own a scroll viewport. Use the
ref to measure or drive the scroll viewport (e.g. a virtualizer's
getScrollElement). |
density | "compact" | "default" | 'default' | Row density. default keeps py-2 cells; compact tightens to py-1
(~32px rows) for data-heavy screens. |
grid | boolean | false | Draw the full spreadsheet grid — a hairline on every cell's trailing edge in addition to the row rules (Wave 2, the Attio data-table voice). Off by default: simple tables keep row rules only. |
headerTone | "ink" | "muted" | 'muted' | Header voice. muted (default) keeps the 12/500 text-label-sm
muted-foreground headers; ink switches to 14/500 foreground headers — the
denser "spreadsheet" read for data-heavy screens. |
scrollLabel | string | the table's `aria-label` | Accessible name for the scroll viewport that wraps the <table>. Defaults
to the table's own aria-label. When a name is available the viewport is
exposed as role="region"; pass one whenever the table can scroll, so the
region a keyboard user lands on announces what it holds. |
Data attributes and CSS variables on Table
| Attribute | Values |
|---|---|
data-density | "compact" |
data-grid | "" |
data-header-tone | "ink" |
data-slot | "table" |
TableHeader, TableBody, TableFooter, TableRow, TableHead,
TableCell, and TableCaption add no props of their own — each accepts
everything its underlying native element accepts (<thead>, <tbody>,
<tfoot>, <tr>, <th>, <td>, <caption>), plus className and ref.
Accessibility
- Built on native table semantics — screen readers announce the table, its caption, headers, and the row/column relationships automatically.
- Use
TableCaption(or anaria-label/aria-labelledbyonTable) to give the table an accessible name. - Set
scope="col"on columnTableHeads (andscope="row"on row headers) so assistive technology maps each cell to the right header. - Selection is expressed with the
data-selectedattribute onTableRow; pair it witharia-selectedwhen the rows are interactive so the state is announced. - Borders-only by design: rows are distinguished by
border-border, keeping the table legible in high-contrast and forced-colors modes. - The scroll viewport is keyboard-reachable exactly when it scrolls, so a wide table can be read
without a pointer (axe
scrollable-region-focusable) and a table that fits adds no dead tab stop. Its focus outline is pulled inside (focus-visible:-outline-offset-2) because the viewport clips its own overflow.
| Key | Action |
|---|---|
| Tab | Move focus through interactive controls inside cells (links, buttons, checkboxes). |
| Enter / Space | Activate the focused control. |
| ← / → | Scroll the focused table viewport horizontally. |
| Contract | States tested |
|---|---|
| Behaviour | default, selected, scrollable |
| Accessibility | semantic-html, labeled, keyboard |
| Visual | default, hover, selected |
Do / Don't
Sortable List
Reorderable rows with pointer drag, keyboard move mode, a lossless row menu, and server-refusable moves — controlled; the host owns the order.
Data Table Parts
The sort header, selection cells, skeleton rows, empty row, column rules and hooks that DataList and DataGrid are both built from.