Table
A styled <table> inside a horizontally scrolling wrapper. It is purely
presentational: sorting, selection and pagination state stay with you (or
with @zuilib/table, the TanStack data grid built on these parts). Every
rule is drawn by the cells (border-separate, zero spacing), so borders
travel with a sticky header.
import Table from '@zuilib/components/table'
Variants
line rules every row. striped paints alternate body rows on --muted.
bordered outlines every cell as a grid.
Sizes
sm for dense data, md for reading. Cell padding comes from
--table-cell-padding-x/y-sm and -md (falling back to spacing multiples)
and the text size from --text-xs / --text-sm; the caption inherits it.
Sortable heads and selectable rows
sortable wraps the head's children in a button with a sort indicator and
sets aria-sort from sortDirection. interactive rows get a pointer and
hover feedback; with an onClick they also become a tab stop where Enter
and Space fire the click. selected paints the row --accent and sets
aria-selected, which assistive tech announces inside a role="grid" table.
Sticky header
stickyHeader pins the header cells to the top of the scroll container.
The wrapper is that container, so give it a height through
containerClassName; otherwise the page scrolls and the header goes with
it. One header row only.
Props
TableProps extends the <table> attributes with:
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'line' | 'striped' | 'bordered' | 'line' | |
size | 'sm' | 'md' | 'md' | Cell padding from --table-cell-padding-x/y-*; text --text-xs / --text-sm |
stickyHeader | boolean | false | Header cells stick to the top of the container; size it with containerClassName |
fullWidth | boolean | true | w-full on the table |
className | string | — | Merged last onto the <table> (the ref target) |
containerClassName | string | — | Merged last onto the scrolling wrapper |
Table.Row
TableRowProps extends the <tr> attributes with:
| Prop | Type | Default | Description |
|---|---|---|---|
selected | boolean | false | --accent paint, data-selected and aria-selected |
interactive | boolean | false | Body rows only: pointer cursor and hover. With onClick also tabIndex=0, a focus ring and Enter / Space activation |
Table.Head
TableHeadProps extends the <th> attributes (minus the deprecated align) with:
| Prop | Type | Default | Description |
|---|---|---|---|
align | 'start' | 'center' | 'end' | 'start' | Text alignment; also steers the sort button content |
sortable | boolean | false | Wraps children in a sort button and sets aria-sort |
sortDirection | 'asc' | 'desc' | 'none' | 'none' | |
onSort | () => void | — | Called when the sort button is activated |
sortIndicator | ReactNode | — | Replaces the default arrows; wrapped in data-slot="table-sort-indicator" with data-sort |
sortButtonClassName | string | — | Merged last onto the sort button |
Table.Cell and Table.Caption
| Prop | Type | Default | Description |
|---|---|---|---|
align | 'start' | 'center' | 'end' | 'start' | On Table.Cell; end for numbers |
side | 'top' | 'bottom' | 'bottom' | On Table.Caption: where the caption renders |
Table.Header, Table.Body and Table.Footer take the plain <thead>,
<tbody> and <tfoot> attributes. All parts are also named exports
(TableRow, TableHead, …).
Slots
| Slot | Element | Notes |
|---|---|---|
[data-slot="table-container"] | div | The scrolling wrapper; data-variant, data-size |
[data-slot="table"] | table | data-variant, data-size, data-sticky-header |
[data-slot="table-header"] | thead | — |
[data-slot="table-body"] | tbody | — |
[data-slot="table-footer"] | tfoot | On --muted, ruled off from the body |
[data-slot="table-row"] | tr | data-selected, data-interactive (present or absent) |
[data-slot="table-head"] | th | scope="col"; data-align, data-sortable, data-sort |
[data-slot="table-sort-button"] | button | Fills a sortable head; data-sort |
[data-slot="table-sort-icon"] | svg | Default indicator; data-sort |
[data-slot="table-sort-indicator"] | span | Wraps a custom sortIndicator; data-sort |
[data-slot="table-cell"] | td | data-align |
[data-slot="table-caption"] | caption | data-side |
/* Consumer CSS: denser tables everywhere */
:root {
--table-cell-padding-x-md: calc(var(--spacing) * 2);
--table-cell-padding-y-md: calc(var(--spacing) * 1.5);
}
Accessibility
- A real
<table>;Table.Headrenders<th scope="col">. Name the table withTable.Captionoraria-label. - A sortable head sets
aria-sort="ascending" | "descending" | "none"and puts its children in a<button type="button">, so sorting is keyboard operable with an inset--ringfocus ring. - An
interactiverow withonClickgetstabIndex=0, afocus-visibleoutline and activates on Enter and Space (Space does not scroll). LeaveonClickoff the row when the action lives on a control inside it, so the row is not a second tab stop. selectedsetsaria-selected, which screen readers announce only insiderole="grid"; pass that role to<Table>for whole-row selection, or convey the state with a checkbox in the row.- Sticky header cells stay opaque (
bg-background) so body rows slide underneath them.