Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Table

Styled semantic table primitives — a scrollable container plus header, body, footer, row, head, cell, and caption parts.

Status
stable
Since
0.1.0
Accessibility pattern
native table semantics

Last updated

InvoiceStatusMethodAmount
INV-001PaidCredit card$250.00
INV-002PendingPayPal$150.00
INV-003OverdueBank transfer$350.00
INV-004PaidCredit 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/table

The 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 — data-slot="table"
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"
<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 a TableScrollRegion (data-slot="table", container is data-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 and data-selected selected state. Rows inside TableHeader do 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-width floor.
  • 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:

A list of your recent invoices.
InvoiceStatusAmount
INV-001Paid$250.00
INV-002Pending$150.00
INV-003Overdue$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.

RefRequestCredit
REQ-4821Storage 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-4822Webhook 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.

InvoiceStatusMethodCustomerEmailIssuedDueAmount
INV-001PaidCredit cardAda Lovelaceada@analytical.dev2026-05-012026-05-15$250.00
INV-002PendingPayPalGrace Hoppergrace@cobol.mil2026-05-032026-05-17$150.00
INV-003OverdueBank transferAlan Turingalan@enigma.uk2026-04-202026-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.

InvoiceStatusAmount
INV-001Paid$250.00
INV-002Pending$150.00
INV-003Overdue$350.00
INV-004Paid$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.

CompanyOwnerARR
GlobexAda Lovelace$1.2M
InitechGrace Hopper$840K
UmbrellaEdsger 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.

PropTypeDefaultDescription
containerPropsReact.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.
gridbooleanfalseDraw 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.
scrollLabelstringthe 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

AttributeValues
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 an aria-label / aria-labelledby on Table) to give the table an accessible name.
  • Set scope="col" on column TableHeads (and scope="row" on row headers) so assistive technology maps each cell to the right header.
  • Selection is expressed with the data-selected attribute on TableRow; pair it with aria-selected when 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.
KeyAction
TabMove focus through interactive controls inside cells (links, buttons, checkboxes).
Enter / SpaceActivate the focused control.
/ Scroll the focused table viewport horizontally.
ContractStates tested
Behaviourdefault, selected, scrollable
Accessibilitysemantic-html, labeled, keyboard
Visualdefault, hover, selected

Do / Don't

Do
Use TableCaption and scope='col' headers, name the viewport with scrollLabel, and reserve whitespace-nowrap for figures and mono values.
Don't
Use a Table for page layout — it's for tabular data only; use a grid/flex layout instead.

On this page