Skip to content

HyTable ​

Usage examples on this page are written for Lit / plain HTML (<hy-table>). The same component ships as HyTable in @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-header when the table scrolls past the screen.
  • Set empty-text to say why the table is empty.
  • Pick one surface: emphasis draws as on hy-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" and vertical-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 a hy-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 ​

html
<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 ​

html
<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 ​

html
<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 ​

html
<hy-table label="Users" loading></hy-table>
<hy-table label="Users" empty-text="No users yet"></hy-table>

API ​

Properties ​

PropertyAttributeTypeDefaultDescription
rows—T[][]Row data. Property-only — not an attribute (arrays can't be reflected).
columns—TableColumn<T>[][]Column definitions. Property-only.
emphasisemphasis'solid' | 'outlined' | 'tinted' | 'plain''solid'Visual chrome weight on the table surface.
dividersdividers'none' | 'header' | 'rows' | 'cells''rows'Cell-divider treatment. Orthogonal to emphasis.
verticalAlignvertical-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.
stripedstripedbooleanfalseZebra-stripe rows.
hoverablehoverablebooleantrueHighlight rows on hover.
stickyHeadersticky-headerbooleanfalseposition: 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.
stickyFirstColumnsticky-first-columnbooleanfalseposition: sticky; left: 0 on the first cell of each row.
captioncaptionstring''Renders a <caption>. When label is set separately the caption is visually hidden.
labellabelstring''aria-label on the table — required if caption is empty.
loadingloadingbooleanfalseRender skeleton rows instead of data.
sortKeysort-keystring | nullnullControlled sort column — component renders the indicator at sortKey.
sortDirectionsort-direction'asc' | 'desc'nullControlled sort direction.
emptyTextempty-textstring'No data'Fallback when rows.length === 0 and no empty slot is provided.
regionLabelregion-labelstring'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.
namedRegionLabelnamed-region-labelstring'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 ​

EventDetailDescription
sort—TableSortDetail — fired when a sortable header is activated.

Slots ​

SlotDescription
defaultThe markup form's body rows, hy-table-row elements (no columns set).
headerThe markup form's header row, an hy-table-row with slot="header".
emptyCustom empty-state content (overrides emptyText).
loadingCustom 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 ​

PartDescription
baseOuter scroll-container wrapper.
tableThe <table> element.
head<thead>.
body<tbody>.
rowEach <tr> (both head and body).
header-cell<th>.
cell<td>.
sort-button<button> inside a sortable <th>.
sort-indicatorArrow icon beside the sort button.
caption<caption>.
emptyContainer 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.

PropertyDefaultDescription
--hy-table-border-subtle-restvar(--hy-border-subtle-rest)Horizontal/vertical border color.
--hy-table-background-surface-mediumvar(--hy-background-surface-medium)Header row background.
--hy-table-header-background-surface-basevar(--hy-background-surface-base)Header row background over a tinted body.
--hy-table-header-label-lg-font-familyvar(--hy-label-lg-font-family)Font family of the column headers (Label LG unless set).
--hy-table-header-label-lg-font-sizevar(--hy-label-lg-font-size)Font size of the column headers.
--hy-table-header-label-lg-font-weightvar(--hy-label-lg-font-weight)Font weight of the column headers.
--hy-table-header-label-lg-letter-spacingvar(--hy-label-lg-letter-spacing)Letter spacing of the column headers.
--hy-table-header-label-lg-line-heightvar(--hy-label-lg-line-height)Line height of the column headers.
--hy-table-header-foreground-default-restvar(--hy-foreground-default-rest)Header row text color.
--hy-table-background-neutral-hovervar(--hy-background-neutral-hover)Row background on hover.
--hy-table-row-stripe-background-surface-subtlevar(--hy-background-surface-subtle)Alternate row background when striped.
--hy-table-padding-layout-mdvar(--hy-padding-layout-md)Horizontal cell padding.
--hy-table-padding-layout-smvar(--hy-padding-layout-sm)Vertical cell padding.
--hy-table-radius-container-mdvar(--hy-radius-container-md)Outer container border-radius.
--hy-table-stroke-smvar(--hy-stroke-sm)Border width.
--hy-table-stroke-nonevar(--hy-stroke-none)No border: the markup form's cells without dividers.
--hy-table-motion-transition-hover-durationvar(--hy-motion-transition-hover-duration)Transition duration for row hover state feedback
--hy-table-motion-transition-hover-easingvar(--hy-motion-transition-hover-easing)Transition easing for row hover state feedback
--hy-table-header-text-transformnoneCase of the column headers (none unless set; uppercase shows them in capitals). Display only — the header text stays as written.

Built with Lit. Documented with VitePress.