Table
A grid of data, rendered from a column list and a row list. There are no <tr>s to write.
import { Table, type TableColumn } from 'neba';
const headers: TableColumn<Deploy>[] = [
{ key: 'environment', label: 'Environment', width: 180 },
{ key: 'duration', label: 'Duration', align: 'end', render: (row) => `${row.duration}m` }
];
<Table headers={headers} items={deploys} getRowKey={(row) => row.id} />;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'outline' | 'text' | 'outline' | Weight of the surface, passed straight to the Box. `text` leaves the rows with no sheet |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Type scale and padding of the cells, and the radius of the sheet |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. The sheet is white, so it reaches the hairline and the rules between rows |
| densityshared | 'default' | 'compact' | 'default' | Padding only — never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. 0 means no shadow at all |
| headers * | readonly TableColumn[] | — | The columns: key, label, width, align and render |
| items * | readonly Row[] | — | The rows |
| getRowKey | (row, index) => Key | index | A stable key per row. Required the moment the table sorts or filters |
| caption | ReactNode | — | Shown above the table, and read as its accessible name |
| empty | ReactNode | 'No data' | What to show instead of rows when items is empty |
| striped | boolean | false | Tints every other row. For a wide table the eye has to track across |
| hoverable | boolean | false | Lights the row under the pointer |
| stickyHeader | boolean | false | Pins the header while the body scrolls. Only does anything if the height is constrained |
| onRowClick | (row, index) => void | — | Makes the rows activatable |
The sheet is a Box — variant, size, color, density and elevation all pass straight through, so a table is styled on the same axes as everything it might sit next to. What Table adds is the part that is genuinely tabular.
TableColumn
interface TableColumn<Row> {
key: string; // identifies the column, and names the property to read
label?: React.ReactNode; // the heading; defaults to the key
width?: number | string; // a number is pixels, a string is any CSS length
align?: 'start' | 'center' | 'end';
render?: (row: Row, index: number) => React.ReactNode;
}This is the whole reason Table takes data rather than markup. A <td> written out per row can silently disagree with the <th> above it about how many there are or what order they come in; a column list cannot.
Examples
Widths and alignment
width is a default: the table still balances its columns to fill the available space, so this is a starting proportion rather than a guarantee. It is written onto a <col> rather than onto the first row's cells — a width set on a <th> is a width the browser renegotiates against every other row, and only the column element states it once.
Numbers usually want align: 'end' so their digits line up.
Rows
striped is for a wide table where the eye has to track across; on a narrow one it is noise. onRowClick makes the rows activatable and turns the hover treatment on with it.
getRowKey defaults to the row's index. That is fine for a static table and wrong for one that sorts or filters — pass it the moment the rows can move.
Empty
The header row
The header sits one step up the sheet's opacity ladder rather than taking a tint: it is still the container, and a coloured band behind a row of column names is the fastest way to make data look like chrome. The rule under it is the same --n-line a Card scores its sections with.
stickyHeader only does anything if something around the table actually constrains its height.
Accessibility
- Renders a real
<table>with<th scope="col">headings. - A
captionis read as the table's accessible name. - The empty state spans every column, so it is announced as one cell rather than as a short first column.