Skip to content
Home Theme Gallery

Table

The custom element is <hui-table>.

Overview

A light-DOM behaviour element wrapped around a <table> the server rendered. It adds a tri-state selection column, sort headers that announce aria-sort, and a sticky header - and never reorders a row.

Example

Sortable and selectable
Source
<hui-table selectable sort="name" sort-direction="ascending" selection="42">
<table class="hui-table">
<thead>
<tr>
<th data-sort-key="name">Name</th>
<th data-sort-key="updated">Updated</th>
</tr>
</thead>
<tbody>
<tr data-row-value="42"><td>Home-UI</td><td>Today</td></tr>
<tr data-row-value="43"><td>Other</td><td>Yesterday</td></tr>
</tbody>
</table>
</hui-table>

The Go template that renders it:

<hui-table selectable sort="{{ .Sort }}" sort-direction="{{ .Direction }}" selection="{{ .Selection }}">
<table class="hui-table">
<thead>
<tr>
<th data-sort-key="name">Name</th>
<th data-sort-key="updated">Updated</th>
</tr>
</thead>
<tbody>
{{ range .Rows }}
<tr data-row-value="{{ .ID }}"><td>{{ .Name }}</td><td>{{ .Updated }}</td></tr>
{{ end }}
</tbody>
</table>
</hui-table>

On a phone

A table wider than its container scrolls sideways inside the element, not the page. While it overflows, the element is a named region in the tab order - named by the table’s <caption> - so a keyboard can scroll it as well as a thumb. It stops being a tab stop once it fits.

Five columns at a phone’s width 375 px wide
Source
<hui-table selectable>
<table class="hui-table">
<caption>Recent invoices</caption>
<thead><tr><th>Invoice</th><th>Status</th><th>Method</th><th>Due date</th><th style="text-align:right">Amount</th></tr></thead>
<tbody>
<tr data-row-value="INV001"><td>INV001</td><td>Paid</td><td>Credit Card</td><td>12 October 2026</td><td style="text-align:right">$250.00</td></tr>
<tr data-row-value="INV002"><td>INV002</td><td>Pending</td><td>PayPal</td><td>14 October 2026</td><td style="text-align:right">$150.00</td></tr>
<tr data-row-value="INV003"><td>INV003</td><td>Unpaid</td><td>Bank Transfer</td><td>20 October 2026</td><td style="text-align:right">$350.00</td></tr>
</tbody>
</table>
</hui-table>

API

Attributes5
Attributes
NameTypeDefaultDescription
selectablebooleanfalseAdds the selection column and its tri-state header control.
stickybooleanfalsePins <thead> while the body scrolls.
sortstring-The data-sort-key the server sorted by. Mirrored to aria-sort on that header.
sort-direction"ascending" | "descending" | "none""none"Which way. The matching header gets aria-sort; the others get none.
selectioncomma-separated <code>data-row-value</code>s""Which rows are selected. The reader changes it; the server sends it back.
Properties5
Properties
NameTypeDefaultDescription
selectablebooleanfalseReflects to selectable.
stickybooleanfalseReflects to sticky.
sortstring''Reflects to sort.
sortDirectionSortDirection'none'Reflects to sort-direction.
selectionstring''Reflects to selection.
Events2
Events
NameDescription
hui-sortA sort header was activated. bubbles, composed and cancelable: preventing it stops the attribute changing, so a host doing a round-trip can wait for the server. Detail: { column, direction }.
hui-selection-changeThe selection changed, after selection is updated. Detail: { values }.
Slots1
Slots
NameDescription
(default)The <table> the server rendered. Rows stay the host’s <tr>.
CSS custom properties2
CSS custom properties
NameDefaultDescription
--hui-cardoklch(1 0 0)Fill behind the sticky header, so rows do not show through.
--hui-borderoklch(0.922 0.005 325.62)Row hairlines.
Classes3
Classes
NameDescription
.hui-table__sortThe sort control hui-table wires into a header. Its indicator shows the current direction from the header’s aria-sort.
<th aria-sort="ascending"><button class="hui-table__sort" data-sort-key="name">Name</button></th>
.hui-table__selectThe selection cell hui-table adds when selectable.
.hui-table__hitThe 24px label hui-table wraps each selection box in, so a thumb has the WCAG 2.5.8 minimum to hit while the box is drawn at 16px (ISS-020).

States

Sticky header

sticky keeps the header in place while the rows scroll under it. The header sticks inside the element's own scroll box, so the example gives the element a height to scroll within.

Sticky header
Source
<hui-table sticky style="display:block;height:8rem;overflow:auto">
<table class="hui-table">
<thead><tr><th data-sort-key="name">Name</th><th>Time</th></tr></thead>
<tbody>
<tr><td>A. Bell</td><td>01:42:07</td></tr>
<tr><td>C. Diaz</td><td>01:44:19</td></tr>
<tr><td>E. Fox</td><td>01:46:02</td></tr>
<tr><td>G. Hall</td><td>01:47:55</td></tr>
</tbody>
</table>
</hui-table>

Server-side mechanics

The server owns the rows, the sort column and direction, and which rows are selected. It renders data-sort-key on each sortable <th> and data-row-value on each <tr>. The element is not form-associated and submits nothing; a selection that drives an action is the server’s, sent back as the selection attribute. An hx-swap replaces the table wholesale and the element re-reads the attributes, so a selection the reader made loses to the one the server sent.

Accessibility

  • The sorted header carries aria-sort, read from the server’s sort/sort-direction, so the indicator is announced.
  • The header checkbox is indeterminate when some rows are selected and checked only when all are.
  • Every selection control has a name, so a screen reader announces the row it belongs to.
  • Sorting is a server round-trip; the element never reorders rows, so no row order changes under a screen-reader user.
  • A table wider than its container scrolls inside the element. While it overflows, the element is a role="region" with tabindex="0", named by the table’s caption or aria-label, so a keyboard can reach and scroll it (WCAG 2.1.1). A role or tabindex the host wrote is left alone.
  • Each selection box is drawn at 16px inside a 24px label, the WCAG 2.5.8 minimum for a pointer.

Keyboard

Keyboard
KeysAction
TabReaches each selection control and sort header in order.
SpaceToggles a selection checkbox.
Enter, SpaceActivates a sort header, emitting hui-sort.

Gotchas

  • The rows are the server’s: the element never sorts, filters or paginates. Activating a header emits hui-sort and nothing moves until the server re-renders.
  • It is light DOM, so .hui-table and the host’s own styles still reach the table. There is no shadow root and no ::part.