Table
Renders a grid of data from a column definition and a row list. There are no <tr>s or <td>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 | — | 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 |
| locale | string | — | BCP 47 tag deciding the strings it draws on its own behalf, and the locale the default sort compares strings in |
| classNames | NebaSlots<'table' | 'caption' | 'head' | 'headCell' | 'body' | 'row' | 'cell' | 'empty'> | — | Class names for the parts behind the root. The root itself is className, so there is no root key |
The outer sheet is a Box: variant · size · color · density · elevation all pass straight through.
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;
}With render you draw the cell yourself; without it, row[key] is printed as-is.
Examples
width and align
width is a starting width. The table still balances its columns to fill the available space, so it acts as a proportion rather than a fixed value. It is written onto a <col>, so it applies consistently to every row.
Numeric columns usually want align: 'end' so their digits line up.
striped · hoverable · onRowClick
striped alternates the row background: useful on a wide table where the eye has to track across; on a narrow one it is noise. onRowClick makes rows activatable and turns the hover treatment on with it.
getRowKey defaults to the row index. Pass it whenever sorting or filtering can reorder the rows.
empty
What to show when items is empty. It renders as a single cell spanning every column.
stickyHeader
Pins the header row while the body scrolls. It only does anything if something around the table constrains its height.
classNames
className is the sheet (the Box the table scrolls horizontally inside), so the <table> and everything in it are reached through classNames.
<Table
headers={headers}
items={items}
classNames={{ table: 'tabular-nums', headCell: 'text-(--neba-fg)', row: 'align-top' }}
/>The slots are table, caption, head, headCell, body, row, cell and empty.
One thing to know before reaching for cell: a cell's padding, alignment and background are written as inline styles rather than as utilities, because a host stylesheet's td rule outranks any one-class utility. A class you hand to headCell, cell or empty can add anything the component does not already set inline (a colour, a font, a border), but changing one of those three needs an important utility (p-4!). See prop conventions.
Accessibility
- Renders a real
<table>with<th scope="col">headings. - A
captionis read as the table's accessible name. - The empty state cell uses
colSpanto cover every column, so it is not announced as short text in the first column. - With
onRowClick, a row enters the tab order, answers Enter and Space, and draws a focus-visible ring. Itsroleis left alone, so the column headings and the row's position are still announced. - A link or a button inside a cell keeps its own keys. The click it raises still bubbles to the row, so call
event.stopPropagation()in that control's handler if the row should not open with it. localedecides what an empty table says;emptywrites it out instead.