Skip to content
Component installs need the registry setup
VegaStack Design

Table

A responsive table component — caption, header, body, footer, row, head and cell parts over semantic table markup.

Status
stable
Since
0.1.0
Accessibility pattern
native table semantics

Last updated

A list of your recent invoices.
InvoiceStatusMethodAmount
INV001PaidCredit Card$250.00
INV002PendingPayPal$150.00
INV003UnpaidBank Transfer$350.00
INV004PaidCredit Card$450.00
INV005PaidPayPal$550.00
INV006PendingBank Transfer$200.00
INV007UnpaidCredit 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 table

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/table

Run 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.

A list of your recent invoices.
InvoiceStatusMethodAmount
INV001PaidCredit Card$250.00
INV002PendingPayPal$150.00
INV003UnpaidBank Transfer$350.00
INV004PaidCredit Card$450.00
INV005PaidPayPal$550.00
INV006PendingBank Transfer$200.00
INV007UnpaidCredit Card$300.00
Total$2,250.00

Anatomy

Table — data-slot="table" | "table-container"
TableBody — data-slot="table-body"
TableCaption — data-slot="table-caption"
TableCell — data-slot="table-cell"
TableFooter — data-slot="table-footer"
TableHead — data-slot="table-head"
TableHeader — data-slot="table-header"
TableRow — data-slot="table-row"
TableScrollRegion — data-slot="table-container"

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
A list of your recent invoices.
InvoiceStatusMethodAmount
INV001PaidCredit Card$250.00
INV002PendingPayPal$150.00

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.

A list of your recent invoices.
InvoiceStatusMethodAmount
INV001PaidCredit Card$250.00
INV002PendingPayPal$150.00
INV003UnpaidBank 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.

ProductPriceActions
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.

StatusMethod
INV001PaidCredit Card$250.00
INV002PendingPayPal$150.00
INV003UnpaidBank Transfer$350.00
INV004PaidCredit Card$450.00
INV005PaidPayPal$550.00
INV006PendingBank Transfer$200.00
INV007UnpaidCredit 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, td and caption elements, 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.
  • TableCaption is the table's accessible name. Every data table should have one, or an aria-label on Table when the name is already visible elsewhere on the page.
  • A sortable column sets aria-sort on its TableHead (ascending, descending or none) 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 a bg-muted fill. Selection is a colour change, so pair it with the checkbox or control that owns it rather than relying on the tint alone.
ContractStates tested
Behaviourdefault, selected
Accessibilitynative-or-base-ui-semantics, browser-accessibility-test
Visualdefault, hover, selected

Do / Don't

Do
Use Table for tabular data — records with the same fields — and caption it so the table announces what it holds.
Don't
Use Table for page layout or for a list of cards; a data grid with editable cells and its own keyboard model is a different component.

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 stays h-10 font-medium text-foreground with no wrap variable), COL-6 (the row hover is upstream's own hover:bg-muted/50) and COL-8 (a selected row is data-[state=selected]:bg-muted, not a pressed rung). The container is upstream's data-slot="table-container" div.
  • DOC-2 — import { cn } from "cn" becomes @vegastack/design; our npm layer owns cn.
  • DOC-1 — prettier reflow plus the registry-stamp.mjs provenance header the three-copy model needs; no decision of its own.

On this page