Table
Styled wrapper around native HTML table. Skin only - bring TanStack Table for sorting, filtering, and pagination.
Overview
Table is intentionally a skin, not a behavior. Eight pieces - Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption - render as native table, thead, tbody, tfoot, tr, th, td, and caption with consistent padding, borders, and hover. The native semantics stay intact, so screen readers and assistive tech treat the markup as a real data table.
For sorting, filtering, pagination, row selection, or virtualization, compose Table with @tanstack/react-table (the DataTable pattern in the Patterns layer). The primitive itself never tries to be a data grid - that lock-in lives at the pattern layer.
Preview
| Item | Category | Deps |
|---|---|---|
| rx-message | AI | 4 |
| rx-button | Forms | 1 |
| rx-card | Display | 0 |
| rx-area-chart | Data | 2 |
| rx-alert | Feedback | 1 |
'use client';import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow,} from '@gremorie/rx-display';const REGISTRY_ITEMS = [ { id: 'rx-message', category: 'AI', deps: 4 }, { id: 'rx-button', category: 'Forms', deps: 1 }, { id: 'rx-card', category: 'Display', deps: 0 }, { id: 'rx-area-chart', category: 'Data', deps: 2 }, { id: 'rx-alert', category: 'Feedback', deps: 1 },];export function TablePreview() { return ( <Table> <TableCaption>A sample of registry items.</TableCaption> <TableHeader> <TableRow> <TableHead>Item</TableHead> <TableHead>Category</TableHead> <TableHead className="text-right">Deps</TableHead> </TableRow> </TableHeader> <TableBody> {REGISTRY_ITEMS.map((item) => ( <TableRow key={item.id}> <TableCell className="font-mono">{item.id}</TableCell> <TableCell>{item.category}</TableCell> <TableCell className="text-right">{item.deps}</TableCell> </TableRow> ))} </TableBody> </Table> );}Anatomy
Table wraps <table> in an overflow container
├─ TableCaption <caption>, rendered below the table
├─ TableHeader <thead>
│ └─ TableRow <tr> with hover + data-[state=selected]
│ └─ TableHead <th> column header
├─ TableBody <tbody>
│ └─ TableRow <tr> with hover + data-[state=selected]
│ └─ TableCell <td> body cell
└─ TableFooter <tfoot> (muted, top border)
└─ TableRow <tr> with hover + data-[state=selected]
└─ TableCell <td> summary cellInstallation
bash npx gremorie@latest add rx-table bash pnpm dlx gremorie@latest add rx-table bash yarn dlx gremorie@latest add rx-table bash bunx --bun gremorie@latest add rx-table Usage
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
} from "@gremorie/rx-display";
export function Example() {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Item</TableHead>
<TableHead>Category</TableHead>
<TableHead className="text-right">Deps</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-mono">rx-card</TableCell>
<TableCell>Display</TableCell>
<TableCell className="text-right">0</TableCell>
</TableRow>
</TableBody>
</Table>
);
}The Angular edition of this component ships from source today (see the workbench for the side-by-side); its registry entry is coming next.
API
<Table>
Wraps the native <table> in a div.relative.w-full.overflow-x-auto container for horizontal scroll on narrow viewports. The table itself is w-full caption-bottom text-sm.
All <table> props are forwarded.
<TableHeader>
Renders as <thead>. Adds [&_tr]:border-b so the row underneath the header always shows the divider.
<TableBody>
Renders as <tbody>. Strips the trailing border on the last row via [&_tr:last-child]:border-0.
<TableFooter>
Renders as <tfoot>. Styled with bg-muted/50 border-t font-medium for sum rows or summary lines.
<TableRow>
Renders as <tr> with hover:bg-muted/50 (interactive feel without claiming interactivity), data-[state=selected]:bg-muted (works with TanStack Table's selection state), and border-b transition-colors.
<TableHead>
Renders as <th>. Defaults: h-10 px-2 text-left align-middle font-medium whitespace-nowrap. Pairs with [role=checkbox] for selection columns.
<TableCell>
Renders as <td>. Defaults: p-2 align-middle whitespace-nowrap. Same checkbox accommodations as TableHead.
<TableCaption>
Renders as <caption>. Defaults: text-muted-foreground mt-4 text-sm. The parent table is caption-bottom, so the caption appears below the table by default.
Composition
<Table>wraps everything. Always include this - the overflow container is what keeps narrow viewports from breaking.<TableCaption>(optional) describes the table content. Place as the first child ofTable- native HTML rule, even though it renders visually at the bottom.<TableHeader>holds one<TableRow>of<TableHead>cells.<TableBody>holds the data rows.<TableFooter>(optional) holds totals, sums, or summary rows.
Every cell should be a TableCell or TableHead - mixing in raw <td> or <th> works but skips the consistent padding and border treatment.
Variations
Data table with caption
| Item | Category | Deps |
|---|---|---|
| rx-message | AI | 4 |
| rx-card | Display | 0 |
| rx-button | Forms | 1 |
'use client';import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow,} from '@gremorie/rx-display';const REGISTRY_ITEMS = [ { id: 'rx-message', category: 'AI', deps: 4 }, { id: 'rx-card', category: 'Display', deps: 0 }, { id: 'rx-button', category: 'Forms', deps: 1 },];export function TableCaptionPreview() { return ( <Table> <TableCaption>Registry inventory by category.</TableCaption> <TableHeader> <TableRow> <TableHead>Item</TableHead> <TableHead>Category</TableHead> <TableHead className="text-right">Deps</TableHead> </TableRow> </TableHeader> <TableBody> {REGISTRY_ITEMS.map((item) => ( <TableRow key={item.id}> <TableCell className="font-mono">{item.id}</TableCell> <TableCell>{item.category}</TableCell> <TableCell className="text-right">{item.deps}</TableCell> </TableRow> ))} </TableBody> </Table> );}Place TableCaption as the first child of Table (native HTML rule). The table is caption-bottom, so it renders below the rows.
With footer summary
| Invoice | Status | Amount |
|---|---|---|
| INV-001 | Paid | $250.00 |
| INV-002 | Pending | $180.00 |
| INV-003 | Paid | $420.00 |
| Total | $850.00 | |
'use client';import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow,} from '@gremorie/rx-display';const INVOICES = [ { id: 'INV-001', status: 'Paid', amount: '$250.00' }, { id: 'INV-002', status: 'Pending', amount: '$180.00' }, { id: 'INV-003', status: 'Paid', amount: '$420.00' },];export function TableWithFooterPreview() { return ( <Table> <TableHeader> <TableRow> <TableHead>Invoice</TableHead> <TableHead>Status</TableHead> <TableHead className="text-right">Amount</TableHead> </TableRow> </TableHeader> <TableBody> {INVOICES.map((invoice) => ( <TableRow key={invoice.id}> <TableCell>{invoice.id}</TableCell> <TableCell>{invoice.status}</TableCell> <TableCell className="text-right">{invoice.amount}</TableCell> </TableRow> ))} </TableBody> <TableFooter> <TableRow> <TableCell colSpan={2}>Total</TableCell> <TableCell className="text-right">$850.00</TableCell> </TableRow> </TableFooter> </Table> );}TableFooter renders as <tfoot> with bg-muted/50 border-t font-medium - ideal for totals or summary rows. Use colSpan to span label columns.
Selected row state
<TableRow data-state={selected ? 'selected' : undefined}>
<TableCell>...</TableCell>
</TableRow>The data-[state=selected]:bg-muted styling picks up automatically. TanStack Table sets this attribute for free when you use its row-selection model.
Accessibility
- Native semantics: every part renders as a real HTML element (
table,thead,tbody,th,td,caption), so screen readers announce row and column relationships correctly. No ARIA needed for static tables. - Captions: use
TableCaptionto summarize the table content in one line ("Registry inventory by category"). Place it as the first child ofTable- native HTML requirement. - Column scope: for tables with row headers, add
scope="row"on the leftmost cell andscope="col"on header cells. Both are native attributes you pass through toTableHead/TableCell. - Selection columns: when a row has a checkbox, the inline
[&:has([role=checkbox])]:pr-0and[&>[role=checkbox]]:translate-y-[2px]styles align the checkbox correctly. Use a realrole="checkbox"or aCheckboxprimitive. - Sortable columns: ARIA sorting (
aria-sort="ascending" \| "descending" \| "none") belongs onTableHead. Wire it up at the DataTable pattern layer. - Sticky headers: native CSS
position: stickyworks on<th>inside<thead>. AddclassName="sticky top-0 bg-background"for scroll-locked headers. - Overflow scroll: the wrapper
div.overflow-x-autoallows horizontal scrolling on narrow viewports - addtabIndex={0}androle="region"on the wrapper if you need keyboard scrolling.
Related
- Card - common host for a Table inside a dashboard tile.
- Pagination - pairs with Table for paginated views.
- Checkbox - drop into selection columns.
- DataTable pattern (Patterns layer) - composes Table with TanStack Table for sorting, filtering, pagination, and selection.