HyTable
Usage examples on this page are written for Lit / plain HTML (
<hy-table>). The same component ships asHyTablein@whitespaceux/harmony-react(native React 19), with typed wrappers for Angular, Solid, Svelte, and Vue. The API reference below applies to all frameworks.
Child components: <hy-table-cell>, <hy-table-row> — used only inside this one.
A semantic, lightweight data table for standard row/column display.
Table, datagrid or list?
A table shows records in columns to read and compare, sorted at most. Records the user filters, selects or pages through are hy-datagrid. Items that are not compared column by column — a feed, people, files, rows to pick from — are hy-list.
Rows are passed as rows: T[] against a columns: TableColumn<T>[] definition. The component is stateless with respect to data — sorting is controlled by the consumer via the sortKey + sortDirection props; the component only emits a sort event and updates the visual indicator / aria-sort when the consumer echoes the state back.
Row density is inherited from the surrounding context — wrap a region in data-density="compact" (or comfort) to shift cell padding without passing a prop per table. There is no local size override; tables are content-dense by design.
When a consumer outgrows hy-table (virtualization, selection, column resizing, multi-sort, pinning, grouping), they swap to hy-datagrid.
Data or markup?
Two forms, one table. Data: set columns and rows, and the table writes every cell (a render function for anything but text). Markup: leave columns unset and write the rows yourself, hy-table-row elements of hy-table-cell elements, a header row with slot="header" — the way to put a field, a badge or a button in each row's cell, from any framework's own markup. The table keeps its look in both: the box, the header band, the lines (dividers), stripes, hover, the sticky header, the empty and loading states. The markup form has no sort button and no sticky first column; the rows are in the order written.
When to use
- Records people read and compare column by column: invoices, users, prices.
- A set small enough to show whole, with no filtering or selecting.
Do
- Align numbers to the end of their column.
- Use
sticky-headerwhen the table scrolls past the screen. - Set
empty-textto say why the table is empty. - Pick one surface:
emphasisdraws as onhy-card— a frame (solid,outlined) or a gray body (tinted), never both on one table. - A form laid out as rows of fields under a header line: the markup form with
dividers="header"andvertical-align="top", so each field's label and message line up.
When not to use
- Data nested in levels that expand and collapse:
hy-tree. - Entries of different shapes, with images or long text: a stack or grid of
hy-card. - A narrow screen: keep few columns with
sticky-first-column, or show the records as ahy-list.
Accessibility: your part
- Give every control in a cell an accessible name that says which row it acts on.
Examples
Sortable table — columns and rows are JS properties
<hy-table label="Team members"></hy-table>
<script>
const table = document.querySelector('hy-table');
table.columns = [
{ key: 'name', label: 'Name', sortable: true },
{ key: 'role', label: 'Role' },
{ key: 'commits', label: 'Commits', sortable: true, align: 'end' },
];
table.rows = [
{ name: 'Alice', role: 'Admin', commits: 128 },
{ name: 'Bob', role: 'Editor', commits: 74 },
{ name: 'Carol', role: 'Viewer', commits: 31 },
];
// Sorting is controlled: sort the data yourself, then echo the state back.
table.addEventListener('sort', (e) => {
const { key, direction } = e.detail;
table.sortKey = key;
table.sortDirection = direction;
table.rows = [...table.rows].sort((a, b) => {
if (direction === null) return 0;
const order = direction === 'asc' ? 1 : -1;
return a[key] < b[key] ? -order : a[key] > b[key] ? order : 0;
});
});
</script>Presentation axes — emphasis, dividers, stripes, sticky header
<hy-table label="Inventory" emphasis="outlined" dividers="cells" striped sticky-header></hy-table>Markup form — rows and cells written as elements, any content in a cell
<hy-table label="Empty states" dividers="cells" emphasis="outlined">
<hy-table-row slot="header">
<hy-table-cell>Type</hy-table-cell>
<hy-table-cell>Title</hy-table-cell>
</hy-table-row>
<hy-table-row>
<hy-table-cell>first use</hy-table-cell>
<hy-table-cell>
<hy-text-input
size="small"
aria-label="First use, title"
value="No projects yet"
></hy-text-input>
</hy-table-cell>
</hy-table-row>
</hy-table>Loading and empty states
<hy-table label="Users" loading></hy-table>
<hy-table label="Users" empty-text="No users yet"></hy-table>API
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
rows | — | T[] | [] | Row data. Property-only — not an attribute (arrays can't be reflected). |
columns | — | TableColumn<T>[] | [] | Column definitions. Property-only. |
emphasis | emphasis | 'solid' | 'outlined' | 'tinted' | 'plain' | 'solid' | Visual chrome weight on the table surface. |
dividers | dividers | 'none' | 'header' | 'rows' | 'cells' | 'rows' | Cell-divider treatment. Orthogonal to emphasis. |
verticalAlign | vertical-align | 'top' | 'middle' | 'middle' | Vertical alignment of the content in every cell. top for rows of fields or wrapping text; a markup cell's own vertical-align wins. |
striped | striped | boolean | false | Zebra-stripe rows. |
hoverable | hoverable | boolean | true | Highlight rows on hover. |
stickyHeader | sticky-header | boolean | false | position: sticky; top: 0 on the header row. The header sticks against the table's OWN scroll-container, so the table must be the vertical scroller: place it in a height-constrained container (a flex item with min-height: 0, or an explicit height) and it caps to that height and scrolls internally. A wrapper with overflow: auto around an unbounded table does NOT work — the table grows to its content and the wrapper scrolls instead, leaving the header with nothing to stick within. |
stickyFirstColumn | sticky-first-column | boolean | false | position: sticky; left: 0 on the first cell of each row. |
caption | caption | string | '' | Renders a <caption>. When label is set separately the caption is visually hidden. |
label | label | string | '' | aria-label on the table — required if caption is empty. |
loading | loading | boolean | false | Render skeleton rows instead of data. |
sortKey | sort-key | string | null | null | Controlled sort column — component renders the indicator at sortKey. |
sortDirection | sort-direction | 'asc' | 'desc' | null | Controlled sort direction. |
emptyText | empty-text | string | 'No data' | Fallback when rows.length === 0 and no empty slot is provided. |
regionLabel | region-label | string | 'Table' | Accessible name of the scrollable region around the table when the table has no name (label, caption, host aria-label / aria-labelledby). Use for internationalization. |
namedRegionLabel | named-region-label | string | 'Table: {label}' | Accessible name of the scrollable region when the table has a name, a template: {label} is replaced by that name. Use for internationalization. |
Events
| Event | Detail | Description |
|---|---|---|
sort | — | TableSortDetail — fired when a sortable header is activated. |
Slots
| Slot | Description |
|---|---|
default | The markup form's body rows, hy-table-row elements (no columns set). |
header | The markup form's header row, an hy-table-row with slot="header". |
empty | Custom empty-state content (overrides emptyText). |
loading | Custom loading placeholder (overrides the default skeletons). |
header-{key} | Override a specific column's header content. Replace {key} with the column key (e.g. header-email). |
cell-{key} | Override a specific column's cell content (static: one element fills the first row only — content per row is a render function or the markup form). Replace {key} with the column key (e.g. cell-status). |
CSS Parts
| Part | Description |
|---|---|
base | Outer scroll-container wrapper. |
table | The <table> element. |
head | <thead>. |
body | <tbody>. |
row | Each <tr> (both head and body). |
header-cell | <th>. |
cell | <td>. |
sort-button | <button> inside a sortable <th>. |
sort-indicator | Arrow icon beside the sort button. |
caption | <caption>. |
empty | Container for the empty-state slot/fallback. |
CSS Custom Properties
The documented hooks. Every override hook the component reads — one per token it paints — is listed in @whitespaceux/harmony/theme-api.json, and @whitespaceux/harmony/component-bindings.json (the Harmony MCP get-bindings tool) says which part and state each one paints.
| Property | Default | Description |
|---|---|---|
--hy-table-border-subtle-rest | var(--hy-border-subtle-rest) | Horizontal/vertical border color. |
--hy-table-background-surface-medium | var(--hy-background-surface-medium) | Header row background. |
--hy-table-header-background-surface-base | var(--hy-background-surface-base) | Header row background over a tinted body. |
--hy-table-header-label-lg-font-family | var(--hy-label-lg-font-family) | Font family of the column headers (Label LG unless set). |
--hy-table-header-label-lg-font-size | var(--hy-label-lg-font-size) | Font size of the column headers. |
--hy-table-header-label-lg-font-weight | var(--hy-label-lg-font-weight) | Font weight of the column headers. |
--hy-table-header-label-lg-letter-spacing | var(--hy-label-lg-letter-spacing) | Letter spacing of the column headers. |
--hy-table-header-label-lg-line-height | var(--hy-label-lg-line-height) | Line height of the column headers. |
--hy-table-header-foreground-default-rest | var(--hy-foreground-default-rest) | Header row text color. |
--hy-table-background-neutral-hover | var(--hy-background-neutral-hover) | Row background on hover. |
--hy-table-row-stripe-background-surface-subtle | var(--hy-background-surface-subtle) | Alternate row background when striped. |
--hy-table-padding-layout-md | var(--hy-padding-layout-md) | Horizontal cell padding. |
--hy-table-padding-layout-sm | var(--hy-padding-layout-sm) | Vertical cell padding. |
--hy-table-radius-container-md | var(--hy-radius-container-md) | Outer container border-radius. |
--hy-table-stroke-sm | var(--hy-stroke-sm) | Border width. |
--hy-table-stroke-none | var(--hy-stroke-none) | No border: the markup form's cells without dividers. |
--hy-table-motion-transition-hover-duration | var(--hy-motion-transition-hover-duration) | Transition duration for row hover state feedback |
--hy-table-motion-transition-hover-easing | var(--hy-motion-transition-hover-easing) | Transition easing for row hover state feedback |
--hy-table-header-text-transform | none | Case of the column headers (none unless set; uppercase shows them in capitals). Display only — the header text stays as written. |