The one table look: neutral header row, whole-cell sortable headers, skeleton, empty and error states, and Pagination.
Table is presentational: you own the rows, the sort state and the page, and it draws them. Columns declare a header, a cell renderer, alignment, width, a monospace flag for references, a skeleton width for loading, and hideBelow to drop a column on narrow screens.
A sortable header is the whole cell, edge to edge: the button fills the th, so hover, press and focus light the full cell over the neutral sweep, never a pill inside it. The sort chevron always holds its slot, so labels never shift when sorting.
Every state is built in: loading draws skeleton rows matching the columns; empty renders whatever you pass (an EmptyState compact that says what to do next); error replaces the body (an Alert with a retry). Pagination sits centred under the table with a summary line.
Two appearances: glass (rows fall through to the frosted panel, the default inside a GlassPanel) and white (an inset solid table for dense lists). Both share one header look, set by --table-* tokens.
Installation
npx shadcn@latest add KezLahd/rime/tableAlso installs: base, icon, skeleton.
Usage
import { Pagination, Table, type Column, type SortState } from "@/components/ui";
const columns: Column<Invoice>[] = [
{ key: "ref", header: "Invoice", cell: (i) => i.reference, mono: true },
{ key: "customer", header: "Customer", sortable: true, cell: (i) => i.customer },
];
<Table caption="Invoices" columns={columns} rows={rows} rowKey={(i) => i.id} sort={sort} onSortChange={setSort} />
<Pagination page={page} pageCount={pageCount} onPageChange={setPage} totalItems={total} pageSize={20} itemLabel="invoices" />Examples
Sortable, with pagination
Loading, empty and error
| Invoice | Customer | Issued | Amount | Status | Actions |
|---|---|---|---|---|---|
White appearance
| Invoice | Customer | Issued | Amount | Status |
|---|---|---|---|---|
| INV-1042 | Jane Cooper | 29 Sep 2026 | $1,280 | Paid |
| INV-1041 | Wade Warren | 27 Sep 2026 | $640 | Sent |
| INV-1040 | Esther Howard | 24 Sep 2026 | $2,150 | Overdue |
API
Generated from the component's TypeScript types and JSDoc (scripts/gen-ui-registry.mjs), so it always matches the source.
Table
| Prop | Type | Default | Description |
|---|---|---|---|
| columnsRequired | ReadonlyArray<Column<T>> | ||
| rowsRequired | ReadonlyArray<T> | ||
| rowKeyRequired | (row: T) => string | ||
| captionRequired | string | Screen-reader caption. Required: a table needs a name. | |
| sort | SortState | null | |
| onSortChange | (sort: SortState) => void | Header click: asc first, then toggles. Sort the rows yourself. | |
| loading | boolean | ||
| skeletonRows | number | 5 | |
| empty | ReactNode | Shown when not loading and rows is empty. Pass an <EmptyState compact />. | |
| error | ReactNode | Replaces the body when the load failed. Pass an <Alert> with a retry. | |
| appearance | "glass" | "white" | "glass" | glass = rows fall through to the frosted panel (no wrapper background). white = the reference white inset table, for dense lists. |
| isRowHighlighted | (row: T) => boolean | Marks a row, e.g. the one just edited. | |
| className | string |
Pagination
| Prop | Type | Default | Description |
|---|---|---|---|
| pageRequired | number | ||
| pageCountRequired | number | ||
| onPageChangeRequired | (page: number) => void | ||
| totalItems | number | ||
| pageSize | number | ||
| itemLabel | string | "results" | Plural noun for the summary: "customers", "requests". |
| className | string |
Theme tokens
Per-component tokens are undeclared by default, so the default shown is the fallback the CSS uses. Set one at :root (or in theme.css) to restyle every instance. Live values are read from this page, so they follow the Rime Studio.
| Token | Default | Live value |
|---|---|---|
| --button-primary-bg | var(--brand-gradient) | … |
| --button-radius | var(--r-sm) | … |
| --inner-radius | var(--r-lg) | … |
| --r-flat | var(--table-radius, var(--r-md)) / var(--table-radius, var(--inner-radius, var(--r-lg))) | … |
| --table-radius | var(--r-md) / var(--inner-radius, var(--r-lg)) | … |
| --table-row-height | 46px | … |
System tokens it reads
| Token | Default | Live value |
|---|---|---|
| --brand-deep | tokens.css | … |
| --brand-gradient | tokens.css | … |
| --control-h-sm | tokens.css | … |
| --dur | tokens.css | … |
| --ease | tokens.css | … |
| --focus-color | tokens.css | … |
| --focus-ring | tokens.css | … |
| --font-mono | tokens.css | … |
| --glass-fill-nested | tokens.css | … |
| --glow-sm | tokens.css | … |
| --ink-body | tokens.css | … |
| --ink-brand | tokens.css | … |
| --ink-disabled | tokens.css | … |
| --ink-inverse | tokens.css | … |
| --ink-muted | tokens.css | … |
| --line-glass | tokens.css | … |
| --line-soft | tokens.css | … |
| --r-lg | tokens.css | … |
| --r-md | tokens.css | … |
| --r-sm | tokens.css | … |
| --rgb-contact | tokens.css | … |
| --surface-solid | tokens.css | … |
| --table-cell-pad | tokens.css | … |
| --table-head-fill | tokens.css | … |
| --table-head-hover | tokens.css | … |
| --table-head-ink | tokens.css | … |
| --table-head-pad | tokens.css | … |
| --table-head-press | tokens.css | … |
| --table-head-rule | tokens.css | … |
| --table-row-hover | tokens.css | … |
| --table-row-line | tokens.css | … |
| --text-caption | tokens.css | … |
| --text-small | tokens.css | … |
| --wash-subtle | tokens.css | … |
Accessibility
- caption is required and rendered as a visually hidden <caption>: a table needs a name.
- Sortable headers are buttons inside the th with aria-sort on the th; the first press sorts ascending, then toggles.
- Rows are not clickable. Put a link (or a ghost Button with href) in the identifying cell, so keyboard and screen-reader users reach it.
- Pagination is a nav labelled "Pagination", the current page is aria-current="page", and prev/next disable at the ends.
Do and don't
Do
- Put the table, its FilterBar and its Pagination in one GlassPanel: that is the list page pattern.
- Right-align numbers and actions (align="right").
- Give every empty state a next step.
Don't
- Never invent another table look; theme --table-* tokens instead.
- Don't uppercase column headers; sentence case only.
- Don't put a title or an "n of m shown" line above the table.
For AI agents
This page as markdown, rendered from the same registry entry. Point an agent at it (or add .md to this page's URL), or at /llms.txt for the index.
Open table.mdRelated: filter-bar, empty-state, glass-panel