Table
A responsive table component — caption, header, body, footer, row, head and cell parts over semantic table markup.
- Status
- Since
0.1.0- Accessibility pattern
- native table semantics
Last updated
| Invoice | Status | Method | Amount |
|---|---|---|---|
| INV001 | Paid | Credit Card | $250.00 |
| INV002 | Pending | PayPal | $150.00 |
| INV003 | Unpaid | Bank Transfer | $350.00 |
| INV004 | Paid | Credit Card | $450.00 |
| INV005 | Paid | PayPal | $550.00 |
| INV006 | Pending | Bank Transfer | $200.00 |
| INV007 | Unpaid | Credit Card | $300.00 |
| Total | $2,250.00 | ||
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Table before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify tableInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/tableRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/use-overflow.
Usage
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">INV001</TableCell>
<TableCell>Paid</TableCell>
<TableCell>Credit Card</TableCell>
<TableCell className="text-right">$250.00</TableCell>
</TableRow>
</TableBody>
</Table>;Table wraps itself in its own data-slot="table-container" element — a relative w-full overflow-x-auto div — so a table that is wider than its column scrolls sideways without any
wrapper at the call site.
| Invoice | Status | Method | Amount |
|---|---|---|---|
| INV001 | Paid | Credit Card | $250.00 |
| INV002 | Pending | PayPal | $150.00 |
| INV003 | Unpaid | Bank Transfer | $350.00 |
| INV004 | Paid | Credit Card | $450.00 |
| INV005 | Paid | PayPal | $550.00 |
| INV006 | Pending | Bank Transfer | $200.00 |
| INV007 | Unpaid | Credit Card | $300.00 |
| Total | $2,250.00 | ||
Anatomy
Examples
Composition
The parts map one to one onto the HTML elements, so the tree is the markup: a caption, a thead
of th, a tbody of td, and an optional tfoot.
Table
├── TableCaption
├── TableHeader
│ └── TableRow
│ ├── TableHead
│ ├── TableHead
│ ├── TableHead
│ └── TableHead
├── TableBody
│ ├── TableRow
│ │ ├── TableCell
│ │ ├── TableCell
│ │ ├── TableCell
│ │ └── TableCell
│ └── TableRow
│ ├── TableCell
│ ├── TableCell
│ ├── TableCell
│ └── TableCell
└── TableFooter| Invoice | Status | Method | Amount |
|---|---|---|---|
| INV001 | Paid | Credit Card | $250.00 |
| INV002 | Pending | PayPal | $150.00 |
Footer
Use the TableFooter component to add a footer to the table. It renders a tfoot, tints itself
with bg-muted/50 and drops the last row's bottom border, so a totals row reads as a summary rather
than as one more record.
| Invoice | Status | Method | Amount |
|---|---|---|---|
| INV001 | Paid | Credit Card | $250.00 |
| INV002 | Pending | PayPal | $150.00 |
| INV003 | Unpaid | Bank Transfer | $350.00 |
| Total | $750.00 | ||
Actions
A table showing actions for each row using a DropdownMenu. Give the trigger a sr-only label —
an icon-only button in a repeated row is otherwise a column of unnamed buttons.
| Product | Price | Actions |
|---|---|---|
| Wireless Mouse | $29.99 | |
| Mechanical Keyboard | $129.99 | |
| USB-C Hub | $49.99 |
Data Table
Table is the presentation layer; the row model is yours. Combine it with a row-model library such
as @tanstack/react-table, or — as here — with ordinary React state, to get sorting, filtering and
pagination. Nothing in this component holds row state, which is why the same parts serve both.
Put the sort control inside TableHead as a Button and mirror the direction onto the header's
aria-sort, so the column announces its state rather than only drawing an arrow.
| Status | Method | ||
|---|---|---|---|
| INV001 | Paid | Credit Card | $250.00 |
| INV002 | Pending | PayPal | $150.00 |
| INV003 | Unpaid | Bank Transfer | $350.00 |
| INV004 | Paid | Credit Card | $450.00 |
| INV005 | Paid | PayPal | $550.00 |
| INV006 | Pending | Bank Transfer | $200.00 |
| INV007 | Unpaid | Credit Card | $300.00 |
| 7 of 7 invoices | |||
RTL
Wrap the subtree in DirectionProvider direction="rtl". TableHead aligns with text-start and the
checkbox padding rule uses pe-0, so the header and the cells flip with the reading direction; an
explicit text-right on a numeric column stays where it is named.
| الفاتورة | الحالة | الطريقة | المبلغ |
|---|---|---|---|
| INV001 | مدفوع | بطاقة ائتمانية | $250.00 |
| INV002 | قيد الانتظار | PayPal | $150.00 |
| INV003 | غير مدفوع | تحويل بنكي | $350.00 |
| INV004 | مدفوع | بطاقة ائتمانية | $450.00 |
| INV005 | مدفوع | PayPal | $550.00 |
| INV006 | قيد الانتظار | تحويل بنكي | $200.00 |
| INV007 | غير مدفوع | بطاقة ائتمانية | $300.00 |
| المجموع | $2,250.00 | ||
API Reference
Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell and
TableCaption add no props of their own — each accepts everything its underlying element accepts
(table, thead, tbody, tfoot, tr, th, td, caption), including colSpan, rowSpan,
scope, aria-sort, className and ref.
Accessibility
- The parts render real
table,thead,tbody,tfoot,tr,th,tdandcaptionelements, so the browser's own table semantics — row and column position, header association, and the screen-reader table cursor — work with no ARIA at all. TableCaptionis the table's accessible name. Every data table should have one, or anaria-labelonTablewhen the name is already visible elsewhere on the page.- A sortable column sets
aria-sorton itsTableHead(ascending,descendingornone) and puts a real button inside it, so the control is reachable and its state is announced. - Cells are
whitespace-nowrap, so a wide table scrolls inside its own container rather than wrapping. Keep the container reachable from the keyboard when it scrolls — a scroll region with no focusable content needs a tab stop of its own. - Selected rows take
data-[state=selected]and abg-mutedfill. Selection is a colour change, so pair it with the checkbox or control that owns it rather than relying on the tint alone.
| Contract | States tested |
|---|---|
| Behaviour | default, selected |
| Accessibility | native-or-base-ui-semantics, browser-accessibility-test |
| Visual | default, hover, selected |
Do / Don't
Deviations
Upstream's file plus packages/ui/upstream/patches/table.patch. Every hunk:
- NO STYLING HUNK — every row this component could have argued with resolves as shadcn and
therefore ships verbatim: LAY-6 (cells stay
whitespace-nowrap, the head staysh-10font-medium text-foregroundwith no wrap variable), COL-6 (the row hover is upstream's ownhover:bg-muted/50) and COL-8 (a selected row isdata-[state=selected]:bg-muted, not a pressed rung). The container is upstream'sdata-slot="table-container"div. - DOC-2 —
import { cn } from "cn"becomes@vegastack/design; our npm layer ownscn. - DOC-1 — prettier reflow plus the
registry-stamp.mjsprovenance header the three-copy model needs; no decision of its own.
Sortable List
Reorderable rows or tiles with pointer drag, touch long-press, keyboard move mode, a remove button, locked rows and server-refusable moves.
Data List
A typed data table, card grid or board — selection, sortable columns, a standard row-actions column, loading, and empty and no-results states.