An accessible data table — a compound of standard HTML table elements (<table>, <thead>, <tbody>, <tr>, <th>, <td>, <caption>) with a tokenised default theme.
Density is set by a data-density ancestor — the Context system, not a Table prop.
Installation
npx primitiv add table
pnpm dlx primitiv add table
yarn dlx primitiv add table
bunx primitiv add table
Import
import{Table}from"@/components/ui/table";
Copied into your project as .primitiv-table — you own the file afterwards, so upgrades are opt-in.
Headless mode installs the npm package instead: @primitiv-ui/react
Anatomy
Nine parts, mapping onto the native table elements. Table is the <table> and owns size / rows. Table.Caption names it; Table.Head / Table.Body / Table.Footer are <thead> / <tbody> / <tfoot>; Table.Row is a <tr>; Table.Header is a <th> and Table.Cell a <td>, both taking align. Table.ScrollArea is a wrapping <div> for wide tables. Use the real semantic parts — a table built from Box/Stack reads as layout, not data, to assistive technology.
Generated from source — headless rows from each *Props type’s JSDoc, styled rows from contract.json. Never hand-maintained.
Table.Root
Extends HTMLTableElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
From
Description
size
"xs" | "sm" | "md" | "lg" | "xl"
md
styled
Type scale for the whole table; data-density scales cell padding further.
rows
"plain" | "striped"
plain
styled
Whether body rows alternate a subtle stripe for readability.
Table.Head
Extends HTMLTableSectionElement — every native attribute of that element is accepted and forwarded.
No props of its own.
Table.Body
Extends HTMLTableSectionElement — every native attribute of that element is accepted and forwarded.
No props of its own.
Table.Footer
Extends HTMLTableSectionElement — every native attribute of that element is accepted and forwarded.
No props of its own.
Table.Row
Extends HTMLTableRowElement — every native attribute of that element is accepted and forwarded.
No props of its own.
Table.Header
Extends HTMLTableCellElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
From
Description
align
"start" | "center" | "end"
start
styled
Column text alignment, matching Figma's Table / Header Cell Align axis. Set it to match the column's cells — end for numeric columns. Direction-aware — start/end follow the reading direction, flipping under RTL.
Table.Cell
Extends HTMLTableCellElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
From
Description
align
"start" | "center" | "end"
start
styled
Cell text alignment, matching Figma's Table / Cell Align axis. CSS cannot align a whole column (text-align does not apply to <col>), so set this on every cell in the column. Direction-aware — start/end follow the reading direction, flipping under RTL.
Table.ScrollArea
Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
From
Description
children* (required)
ReactNode
—
headless
The table (typically a single TableRoot) to
wrap in the horizontally-scrolling container.
style
CSSProperties
—
headless
Merged with (and taking priority over) the base scroll styles
(display: block, overflow-x: auto, max-width: 100%), so
additional styles can be layered on without repeating the scroll
declarations. See TableScrollArea's custom-styles example.
Table.Caption
Extends HTMLTableCaptionElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
From
Description
children* (required)
ReactNode
—
headless
The visible, accessible label for the table. Preferred over
aria-label/aria-labelledby on the table itself — the browser
programmatically associates a <caption> with its <table>.
captionSide
"top" | "bottom"
"bottom"
headless
Controls the CSS caption-side property, positioning the caption
above or below the table. See TableCaption's placement
examples.
- "bottom" — caption appears below the table.
- "top" — caption appears above the table.
Styling contract
17 CSS custom properties on .primitiv-table — mode-agnostic. These names are the stable surface; the values are not.
Emitted automatically by the headless primitive — style against these rather than adding your own state classes. Grouped by the part that emits them, since most are emitted by more than one.
TableRow
className:.primitiv-table__row
Attribute
Value
When
data-state
expanded | collapsed
whether this row's detail row is open
Table.Row
Attribute
Value
When
data-state
expanded | collapsed
whether this row's detail row is open
Accessibility
Name the table. A Table.Caption (or an aria-label on the root) gives the table an accessible name, so a screen-reader user landing on it knows what it holds before reading cells. Table.Caption also takes captionSide to place it visually above or below while staying the table's caption semantically.
Header cells are <th>, and they carry scope.Table.Header renders a real <th>; column headers should be scope="col" (the default for a head-row header) and a row's leading label cell scope="row", so assistive technology can announce the row and column a cell belongs to. Use header cells for headers — never a styled <td>.
align is visual only. It aligns the text; it changes nothing in the accessibility tree, and it is per-cell because CSS cannot align a whole column — set it on the header and every cell alike, or the column won't line up.
A wide table needs a keyboard-scrollable region.Table.ScrollArea should be focusable (tabIndex={0}) and labelled (aria-label) so a keyboard user can scroll to the off-screen columns — a scroll region reachable only by mouse hides data from them.
Striped rows are decoration.rows="striped" conveys nothing to assistive technology; do not lean on the shading to communicate state (a failed row, say) — put that in the cell's content.
Need expandable/disclosure rows? That lives in the headless Table primitive (Table.Expandable / Table.ExpandTrigger / Table.DetailRow), which wires the trigger's aria-expanded / aria-controls to the detail row. The styled registry surface documented here doesn't include it — reach for @primitiv-ui/react directly when you need it.
Don't build tables out of layout primitives. A grid of Box/Stack looks like a table but reads as ungrouped text — no row/column relationships, no navigation. Use the real Table parts whenever the data is genuinely tabular.
Examples
Every example below reacts to the density control. Currently showing Styled mode.
A basic table
The core shape: a Table.Head of Table.Header cells over a Table.Body of Table.Rows and Table.Cells, with a Table.Caption naming the whole thing. The parts render the real <table> / <thead> / <th> / <td> elements, so the browser and assistive technology get a genuine data table — row and column relationships and all — for free.
rows="striped" on the root shades alternate body rows, which helps the eye track across a wide or dense table. It is purely visual — the stripes carry no meaning and are not announced — so reach for it when scanning is hard, not as decoration on a three-row table.
Numbers read best right-aligned, so their digits line up. align is a per-cell prop, not a column one — CSS text-align does not apply to a <col>, so set align="end" on the header and every cell in that column. start / end are direction-aware and flip under RTL, so a numeric column stays edge-aligned in either reading direction.
A table wider than its container should scroll rather than squeeze or overflow the page. Wrap it in Table.ScrollArea — a focusable, labelled scroll region — so a wide table pans horizontally on its own. Give the region an aria-label and it becomes keyboard-scrollable, so the columns past the edge are reachable without a mouse.
Project
Owner
Status
Updated
Commits
Design system
Ada L.
Active
2 days ago
128
Docs site
Grace H.
Active
2 days ago
342
Engine
Alan T.
Active
2 days ago
87
import{Table,TableHead,TableBody,TableRow,TableHeader,TableCell,TableCaption}from"@/components/ui/table";<TableScrollAreaaria-label="Projects"tabIndex={0}><Table>{/* many columns */}</Table></TableScrollArea>
import{Table}from"@primitiv-ui/react";<Table.ScrollAreaaria-label="Projects"tabIndex={0}><Table.Root>{/* many columns */}</Table.Root></Table.ScrollArea>