Table
Styled primitives for tabular data: a real <table> in a hairline-ringed frame that scrolls horizontally when it runs out of room. Sorting, pagination and selection logic stay in your code.
| Invoice | Customer | Status | Amount |
|---|---|---|---|
| INV-1042 | Northwind | Paid | 1,240.00 |
| INV-1043 | Globex | Due | 860.50 |
| INV-1044 | Initech | Overdue | 3,120.00 |
<Table scrollLabel="Invoices">
<TableHead>
<TableRow>
<TableHeader>Invoice</TableHeader>
<TableHeader>Customer</TableHeader>
<TableHeader>Status</TableHeader>
<TableHeader align="end">Amount</TableHeader>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>INV-1042</TableCell>
<TableCell>Northwind</TableCell>
<TableCell><Badge tone="success">Paid</Badge></TableCell>
<TableCell align="end">1,240.00</TableCell>
</TableRow>
{/* … */}
</TableBody>
</Table>Import
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from "@merid/react";Examples
Dense, without hover
density="sm" tightens cells to 10px × 12px; hoverable={false} turns off the row hover tint for read-only tables.
| Key | Requests |
|---|---|
| api/search | 12,480 |
| api/users | 3,904 |
<Table density="sm" hoverable={false} scrollLabel="API keys">
<TableHead>
<TableRow>
<TableHeader>Key</TableHeader>
<TableHeader align="end">Requests</TableHeader>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>api/search</TableCell>
<TableCell align="end">12,480</TableCell>
</TableRow>
</TableBody>
</Table>Row selection
selected on a row applies the accent-soft fill. Pair it with a Checkbox column so selection is operable and announced. scrollLabel makes the frame a focusable, named region so keyboard users can scroll it.
| Invoice | Amount | |
|---|---|---|
| INV-1042 | 1,240.00 | |
| INV-1043 | 860.50 | |
| INV-1044 | 3,120.00 |
<Table scrollLabel="Invoices">
<TableHead>
<TableRow>
<TableHeader><Checkbox aria-label="Select all" checked={allSelected} indeterminate={someSelected} onChange={toggleAll} /></TableHeader>
<TableHeader>Invoice</TableHeader>
<TableHeader align="end">Amount</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<TableRow key={row.id} selected={selected.includes(row.id)}>
<TableCell><Checkbox aria-label={"Select " + row.id} checked={selected.includes(row.id)} onChange={() => toggle(row.id)} /></TableCell>
<TableCell>{row.id}</TableCell>
<TableCell align="end">{row.amount}</TableCell>
</TableRow>
))}
</TableBody>
</Table>Sortable columns
Give a header sortDirection and onSort: it sets aria-sort on the <th> and wraps the label in a real button with a direction icon. Sorting the rows stays yours; use "none" on sortable columns that are not the active sort.
const [sort, setSort] = useState<{ key: "name" | "amount"; dir: "ascending" | "descending" }>({ key: "name", dir: "ascending" });
const toggle = (key: "name" | "amount") =>
setSort((s) => ({ key, dir: s.key === key && s.dir === "ascending" ? "descending" : "ascending" }));
<TableHeader sortDirection={sort.key === "name" ? sort.dir : "none"} onSort={() => toggle("name")}>
Name
</TableHeader>API reference
Every part forwards its ref to the matching native element and passes native attributes through.
Table
The frame div wraps the <table>; className and native props go on the <table>.
| Prop | Type | Default | Description |
|---|---|---|---|
density | "sm" | "md" | "md" | Row density: md 14px 16px cells, sm 10px 12px. |
hoverable | boolean | true | Highlights rows on hover. |
scrollLabel | string | – | Makes the scroll wrapper a focusable region with this name. Omit for tables that never overflow. |
TableHead, TableBody
<thead> (on the subtle surface) and <tbody>. No props beyond native attributes.
TableRow
| Prop | Type | Default | Description |
|---|---|---|---|
selected | boolean | – | Accent-soft fill via `data-selected`. Sets no ARIA: convey selection through the selection control (e.g. a checked checkbox cell), and pass `aria-selected` yourself only on a `role="grid"` table. |
TableHeader
| Prop | Type | Default | Description |
|---|---|---|---|
align | "start" | "center" | "end" | "start" | Text alignment; use end for numeric columns. |
scope | "col" | "row" | "colgroup" | "rowgroup" | "col" | Native header scope. |
sortDirection | "ascending" | "descending" | "none" | – | Makes the column sortable: sets aria-sort and renders the label inside a button with a direction icon. |
onSort | (event) => void | – | Called when the sort button is pressed. Compute the next direction and sort the rows yourself. |
TableCell
| Prop | Type | Default | Description |
|---|---|---|---|
align | "start" | "center" | "end" | "start" | Text alignment; use end for numeric columns. |
Styling
| Hook | Values |
|---|---|
.mrd-table-frame | Scrollable, hairline-ringed wrapper |
.mrd-table | The <table>; data-density = sm · md, data-hoverable |
.mrd-table__head, .mrd-table__body | Sections |
.mrd-table__row | Rows; data-selected when selected |
.mrd-table__header, .mrd-table__cell | Cells; data-align = start · center · end |
Component variable: --mrd-table-cell-padding, set per density.
Accessibility
- It is a native table, so screen readers get row and column navigation for free.
TableHeaderdefaults toscope="col"; usescope="row"for row headers. - Add a
<caption>(it can be visually hidden with VisuallyHidden) oraria-labelon the table to describe it. TableRow selectedis visual only (data-selected); a plain table has no selection semantics, so the selection control in the row must carry the state.- When the table can overflow, set
scrollLabelso the frame is reachable with Tab and scrollable with the arrow keys.
| Key | Action |
|---|---|
| Tab | Moves focus to the scroll region (when scrollLabel is set) and to interactive content in cells. |
| ←→ | Scrolls the focused region horizontally. |
Guidelines
Do
Right-align numbers with align="end" so digits line up; cells already use tabular numerals.
Avoid
Use a table for layout, or centre-align text columns.
Related
- Checkbox — row selection.
- Pagination — paging long tables.
- EmptyState — when a table has no rows.