Search for a command to run...
Semantic HTML table primitives — header / body / row / cell, with a footer slot for summary rows and a `data-state="selected"` hook for row selection. Style with utility classes; structure stays accessible by default.
These are thin semantic wrappers — no built-in sorting, selection, or pagination. For interactive tables, pair these primitives with TanStack Table (the audience / knowledge data-tables in features already do this).
| Name | Role | Status | |
|---|---|---|---|
| Alice Chen | alice@example.com | Owner | Active |
| Ben Patel | ben@example.com | Editor | Active |
| Casey Wu | casey@example.com | Editor | Pending |
| Devi Rao | devi@example.com | Viewer | Active |
| Devi Rao | Viewer | 2024-09-08 |
| Casey Wu | Editor | 2024-06-21 |
| Ben Patel | Editor | 2024-03-04 |
| Alice Chen | Owner | 2024-01-12 |
| Invoice | Customer | Amount |
|---|---|---|
| INV-001 | Acme | $1,200 |
| INV-002 | Globex | $800 |
| INV-003 | Initech | $450 |
| INV-004 | Hooli | $2,100 |
| Total | $4,550 | |
| Project | Owner | |
|---|---|---|
| Onboarding flow | Alice | |
| Pricing page | Ben | |
| Docs migration | Casey | |
| Customer report | Devi |
TableWraps the <table> in an overflow-x-auto container so wide tables scroll horizontally on narrow viewports. Forwards every native <table> prop.
TableHeader · TableBody · TableFooterPass-through wrappers around <thead> / <tbody> / <tfoot>. TableFooter ships with border-t and a subtle bg-sand-9/10 tint so summary rows read as a tier above the body — use for totals, aggregate counts, or "showing 4 of 24" summaries.
TableRowA <tr> with built-in row chrome:
| State | Selector | Background |
|---|---|---|
| Default | — | transparent |
| Hover | :hover (body only) | bg-sand-9/10 |
| Expanded | aria-expanded="true" | bg-sand-9/10 |
| Selected | data-state="selected" | bg-sand-9/15 |
Hover is scoped to <tbody> rows only — header and footer rows stay flat on pointer-over. The data-state="selected" hook is what TanStack Table writes when a row is selected, so wrapping data tables get the highlight for free.
TableHeadA <th> with header typography (text-xs font-medium text-fg-mid — normal case, no uppercase, no letter-spacing). Use inside TableHeader > TableRow. For sortable columns, swap TableHead for TableSortableHead.
TableCellA <td> with default padding (p-3) and whitespace-nowrap. Override className="whitespace-normal" for long-text cells that should wrap.
TableCaptionA <caption> element styled text-sm text-fg-mid with a top margin. Renders below the table by default (caption-bottom).
TableSortableHeadA <th> that replaces TableHead for sortable columns. Renders a ghost-style <button> inside with the column label and an ArrowUp / ArrowDown icon when active, and writes aria-sort on the <th> itself so assistive tech reads the column's sort state.
| Prop | Type | Default | Description |
|---|---|---|---|
direction | "asc" | "desc" | undefined | — | Current sort state for this column. undefined = not sorted; "asc"/"desc" = sorted and shows the matching arrow. Also drives aria-sort="ascending" | "descending" on the <th>. |
onClick | (event) => void | — | Fires on click. The parent owns sort state — typically toggles direction between asc/desc when the same column is clicked again. |
When direction is set, the label lifts to text-fg-high so the active column reads at a glance. Hover state matches control.ghost (bg-sand-9/10 + text-fg-high).
Use the primitives directly when the table is static (a few rows, no sorting, no selection). The
wrapping Table already provides overflow-x-auto for narrow viewports.
Compose with TanStack Table for interactive data tables — sorting, selection, pagination,
virtualization. The data-state="selected" hook is wired so selection highlight works without
extra CSS.
Right-align numeric columns (className="text-right" on the matching TableHead + TableCell)
so digits stack on the decimal place.
Use TableSortableHead for every sortable column. Same visual across audience, knowledge, team
members — keeps the sortable affordance consistent and wires up aria-sort automatically.
Hand-roll a sortable header by dropping a custom <button> into TableHead. TableSortableHead
is the canonical entry point — it carries the active-column lift, the arrow direction, and the
aria-sort attribute on the right element (<th>).
Lay out non-tabular content with Table. Cards, forms, and key-value displays all read worse
here than in a flex container.
Hand-roll selection highlight. The data-state="selected" hook is the canonical entry point —
everything in features that has selection (audience, knowledge) wires into it.
Force every cell to whitespace-nowrap then add horizontal scroll for long text. Override the
default with whitespace-normal on cells whose content should wrap (descriptions, names).