# <aihio-table>

A data table: records in rows and columns, compared by scanning down a column. It wraps a native <table> and gives it the system's styles, sortable columns, and a scroll box the keyboard can reach when the table is too wide for the screen. Use it for records people compare (invoices, members, deployments); use aihio-grid of cards for a few summaries, and never a table for layout. For more rows than a page can hold, show one page at a time with aihio-pagination, or use aihio-data-grid.

Intents: `tabular-data`.

## Attributes

- `density` (`default` | `compact`): Row spacing. compact fits more rows on screen, for dense data that people scan. Default: `default`.
- `sticky-header` (boolean): Keeps the header in view while the rows scroll. The table scrolls inside its own box, which grows to --aihio-table-max-height (min(32rem, 75dvh) unless you set it) before its rows scroll. Default: `false`.
- `manual-sort` (boolean): Leaves the order of the rows to your code. A click on a sortable header still marks it with aria-sort, announces it, and fires aihio-sort, but the rows stay as rendered: sort them yourself, as a framework re-rendering from state or a request to the server does. Without it the table reorders its own rows. Default: `false`.
- `loading` (boolean): Marks the rows as stale while new ones load, such as after a sort sent to the server: they are dimmed, and the table has aria-busy. Default: `false`.
- `sort-ascending-text` (string): Announced after sorting a column ascending, with {column} for the header's text. For localisation (e.g. "Lajiteltu: {column}, nouseva"). Default: `Sorted by {column}, ascending`.
- `sort-descending-text` (string): Announced after sorting a column descending, with {column} for the header's text. Default: `Sorted by {column}, descending`.

## Properties

- `table` (`HTMLTableElement | null`, read-only): The <table> the element enhances.

## Methods

- `sort()`: Sorts by the column whose data-sortable value, or header text, is column: ascending unless direction says otherwise. It moves aria-sort to that header and, without manual-sort, reorders the rows. Like a value set from script, it fires no aihio-sort and announces nothing.

## Events

- `aihio-sort` (detail: column: string, direction: 'ascending' | 'descending'): Fired when the person sorts by a column, once its header carries the new aria-sort and, without manual-sort, the rows are in their new order. column is the header's data-sortable value, or its text when that is empty. Not fired by sort().

## Native elements

<aihio-table> reads these attributes on the native elements inside it.

### <th>

A header cell. In <thead> it names a column; in a body row, with scope="row", it names the row.

#### <th> attributes

- `data-sortable` (string): Makes the column sortable: the header's content becomes a button that sorts the rows by it, ascending first and then descending. The value names the column for aihio-sort and sort(), and can be left empty when nothing reads it. Only on a <th> in <thead>.
- `aria-sort` (`ascending` | `descending` | `none` | `other`): The order the rows are in. Write it on the header the rows are sorted by when the page loads; without manual-sort the table puts the rows in that order. The table moves it from header to header as people sort.
- `data-numeric` (boolean): Lines a column of figures up by place value: end-aligned, in tabular digits. Put it on the column's header and on each of its cells. Default: `false`.
- `data-sort-value` (string): What a row header sorts by, when its text does not sort the way it reads. See <td>.

### <td>

A data cell.

#### <td> attributes

- `data-sort-value` (string): What the cell sorts by when its text does not sort the way it reads, such as a priority ("High" before "Low") or a date written out. A <time datetime> in the cell is read the same way, so a date marked up as one needs nothing more. Figures written for the page's language ("€1,250.50") are read as numbers without it.
- `data-numeric` (boolean): Lines the figure up with the rest of its column. See <th>. Default: `false`.

## Accessibility obligations

- (error, table-accessible-name) When the table has no caption, aria-label, or aria-labelledby: Name the table: a <caption> as its first child, aria-labelledby pointing at the visible heading above it, or aria-label. An unnamed table is announced only as "table", and so is the scroll box it gets on a narrow screen.
- (error, table-header-cells) When the table has no <th> cells: Name each column with a <th> in a <thead> row, and each row with <th scope="row"> where one cell identifies it. Bold text in an ordinary cell is not a header.
- (error, table-click-handler) When a row, header, or cell responds to clicks and holds no link or button: Sort with data-sortable on the <th>, and put a link or button in the row for anything else, such as a link in the cell that names it. A click handler on a row or cell is reachable by pointer only.
- (warn, table-sort-column-name) When manual-sort is set and a sortable header has no column name: Give each sortable header a name, data-sortable="amount", so aihio-sort says which column to sort by without depending on the header's wording.

## Examples

### Records with a caption

Each column is named by a <th> in <thead>, and the cell that names each row is a <th scope="row">. data-numeric lines figures up by place value, and a <tfoot> holds the totals.

```html
<aihio-table>
  <table>
    <caption>Invoices</caption>
    <thead>
      <tr>
        <th scope="col">Invoice</th>
        <th scope="col">Customer</th>
        <th scope="col">Status</th>
        <th scope="col" data-numeric>Amount</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">INV-1042</th>
        <td>Northwind Traders</td>
        <td><aihio-badge variant="success">Paid</aihio-badge></td>
        <td data-numeric>€1,250.00</td>
      </tr>
      <tr>
        <th scope="row">INV-1043</th>
        <td>Fabrikam</td>
        <td><aihio-badge variant="warning">Due</aihio-badge></td>
        <td data-numeric>€980.50</td>
      </tr>
      <tr>
        <th scope="row">INV-1044</th>
        <td>Contoso</td>
        <td><aihio-badge variant="destructive">Overdue</aihio-badge></td>
        <td data-numeric>€12,400.00</td>
      </tr>
    </tbody>
    <tfoot>
      <tr>
        <th scope="row" colspan="3">Total</th>
        <td data-numeric>€14,630.50</td>
      </tr>
    </tfoot>
  </table>
</aihio-table>
```

### Sortable columns

data-sortable makes a header a sort button. The rows load sorted by the header with aria-sort. A <time datetime> sorts by its machine-readable value, and data-sort-value gives a priority an order its words do not have.

```html
<aihio-table>
  <table>
    <caption>Deployments</caption>
    <thead>
      <tr>
        <th scope="col" data-sortable="service">Service</th>
        <th scope="col" data-sortable="priority">Priority</th>
        <th scope="col" data-sortable="deployed" aria-sort="descending">Deployed</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (s)</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">api-gateway</th>
        <td data-sort-value="1">High</td>
        <td><time datetime="2026-10-05T14:20">5 Oct, 14:20</time></td>
        <td data-numeric>42.5</td>
      </tr>
      <tr>
        <th scope="row">billing-worker</th>
        <td data-sort-value="3">Low</td>
        <td><time datetime="2026-10-04T09:05">4 Oct, 09:05</time></td>
        <td data-numeric>118.0</td>
      </tr>
      <tr>
        <th scope="row">search-indexer</th>
        <td data-sort-value="2">Medium</td>
        <td><time datetime="2026-09-29T17:45">29 Sep, 17:45</time></td>
        <td data-numeric>7.25</td>
      </tr>
    </tbody>
  </table>
</aihio-table>
```

### Compact, with a sticky header

density="compact" fits more rows on screen, and sticky-header keeps the column names in view as they scroll. --aihio-table-max-height sets how tall the box grows first.

```html
<aihio-table density="compact" sticky-header style="--aihio-table-max-height: 16rem">
  <table>
    <caption>Requests</caption>
    <thead>
      <tr>
        <th scope="col">Time</th>
        <th scope="col">Method</th>
        <th scope="col">Path</th>
        <th scope="col" data-numeric>Status</th>
        <th scope="col" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td><time datetime="2026-10-06T09:41:02">09:41:02</time></td>
        <td>GET</td>
        <td>/api/projects</td>
        <td data-numeric>200</td>
        <td data-numeric>38</td>
      </tr>
      <tr>
        <td><time datetime="2026-10-06T09:41:03">09:41:03</time></td>
        <td>GET</td>
        <td>/api/projects/42</td>
        <td data-numeric>200</td>
        <td data-numeric>21</td>
      </tr>
      <tr>
        <td><time datetime="2026-10-06T09:41:07">09:41:07</time></td>
        <td>POST</td>
        <td>/api/deployments</td>
        <td data-numeric>201</td>
        <td data-numeric>412</td>
      </tr>
      <tr>
        <td><time datetime="2026-10-06T09:41:09">09:41:09</time></td>
        <td>GET</td>
        <td>/api/deployments/97</td>
        <td data-numeric>200</td>
        <td data-numeric>17</td>
      </tr>
      <tr>
        <td><time datetime="2026-10-06T09:41:12">09:41:12</time></td>
        <td>PATCH</td>
        <td>/api/projects/42</td>
        <td data-numeric>200</td>
        <td data-numeric>64</td>
      </tr>
      <tr>
        <td><time datetime="2026-10-06T09:41:15">09:41:15</time></td>
        <td>GET</td>
        <td>/api/members</td>
        <td data-numeric>200</td>
        <td data-numeric>29</td>
      </tr>
      <tr>
        <td><time datetime="2026-10-06T09:41:18">09:41:18</time></td>
        <td>DELETE</td>
        <td>/api/tokens/7</td>
        <td data-numeric>204</td>
        <td data-numeric>33</td>
      </tr>
      <tr>
        <td><time datetime="2026-10-06T09:41:21">09:41:21</time></td>
        <td>GET</td>
        <td>/api/billing</td>
        <td data-numeric>503</td>
        <td data-numeric>1204</td>
      </tr>
      <tr>
        <td><time datetime="2026-10-06T09:41:22">09:41:22</time></td>
        <td>GET</td>
        <td>/api/billing</td>
        <td data-numeric>200</td>
        <td data-numeric>88</td>
      </tr>
      <tr>
        <td><time datetime="2026-10-06T09:41:30">09:41:30</time></td>
        <td>POST</td>
        <td>/api/invites</td>
        <td data-numeric>422</td>
        <td data-numeric>45</td>
      </tr>
      <tr>
        <td><time datetime="2026-10-06T09:41:31">09:41:31</time></td>
        <td>POST</td>
        <td>/api/invites</td>
        <td data-numeric>201</td>
        <td data-numeric>97</td>
      </tr>
      <tr>
        <td><time datetime="2026-10-06T09:41:36">09:41:36</time></td>
        <td>GET</td>
        <td>/api/audit</td>
        <td data-numeric>200</td>
        <td data-numeric>143</td>
      </tr>
    </tbody>
  </table>
</aihio-table>
```

### Sorted by your code

manual-sort leaves the rows to you: listen for aihio-sort and render them sorted by event.detail.column and event.detail.direction, as a framework re-rendering from state or a request to the server does. The table still marks the header and announces the sort.

```html
<aihio-table manual-sort>
  <table>
    <caption>Customers</caption>
    <thead>
      <tr>
        <th scope="col" data-sortable="name" aria-sort="ascending">Name</th>
        <th scope="col" data-sortable="plan">Plan</th>
        <th scope="col" data-sortable="seats" data-numeric>Seats</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">Contoso</th>
        <td>Business</td>
        <td data-numeric>120</td>
      </tr>
      <tr>
        <th scope="row">Fabrikam</th>
        <td>Team</td>
        <td data-numeric>18</td>
      </tr>
      <tr>
        <th scope="row">Northwind Traders</th>
        <td>Enterprise</td>
        <td data-numeric>740</td>
      </tr>
    </tbody>
  </table>
</aihio-table>
```

### Links and row actions

The cell that names each row links to it, and each row's menu is named for its row, so a screen reader can tell the menus apart.

```html
<aihio-table>
  <table>
    <caption>Team members</caption>
    <thead>
      <tr>
        <th scope="col">Name</th>
        <th scope="col">Role</th>
        <th scope="col">Actions</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row"><a href="/team/ada">Ada Lovelace</a></th>
        <td>Owner</td>
        <td>
          <aihio-dropdown align="end">
            <aihio-button slot="trigger" variant="ghost" size="icon" aria-label="Actions for Ada Lovelace">&#x22ef;</aihio-button>
            <aihio-dropdown-item>Change role</aihio-dropdown-item>
            <aihio-dropdown-item>Remove from team</aihio-dropdown-item>
          </aihio-dropdown>
        </td>
      </tr>
      <tr>
        <th scope="row"><a href="/team/grace">Grace Hopper</a></th>
        <td>Admin</td>
        <td>
          <aihio-dropdown align="end">
            <aihio-button slot="trigger" variant="ghost" size="icon" aria-label="Actions for Grace Hopper">&#x22ef;</aihio-button>
            <aihio-dropdown-item>Change role</aihio-dropdown-item>
            <aihio-dropdown-item>Remove from team</aihio-dropdown-item>
          </aihio-dropdown>
        </td>
      </tr>
    </tbody>
  </table>
</aihio-table>
```

## Mistakes

### An unnamed table is announced only as "table". On a narrow screen it also scrolls inside a box with no name. Give it a <caption>, or point aria-labelledby at the heading above it.

Don't (aihio lint: table-accessible-name):

```html
<aihio-table>
  <table>
    <thead>
      <tr>
        <th scope="col">Invoice</th>
        <th scope="col" data-numeric>Amount</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">INV-1042</th>
        <td data-numeric>€1,250.00</td>
      </tr>
    </tbody>
  </table>
</aihio-table>
```

Do:

```html
<aihio-table>
  <table>
    <caption>Invoices</caption>
    <thead>
      <tr>
        <th scope="col">Invoice</th>
        <th scope="col" data-numeric>Amount</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">INV-1042</th>
        <td data-numeric>€1,250.00</td>
      </tr>
    </tbody>
  </table>
</aihio-table>
```

### Bold text in an ordinary cell looks like a header but is not one. A screen reader moving down a column cannot say which column it is in. Put the column names in <th> cells in a <thead>.

Don't (aihio lint: table-header-cells):

```html
<aihio-table>
  <table>
    <caption>Invoices</caption>
    <tr>
      <td><strong>Invoice</strong></td>
      <td><strong>Amount</strong></td>
    </tr>
    <tr>
      <td>INV-1042</td>
      <td>€1,250.00</td>
    </tr>
  </table>
</aihio-table>
```

Do:

```html
<aihio-table>
  <table>
    <caption>Invoices</caption>
    <thead>
      <tr>
        <th scope="col">Invoice</th>
        <th scope="col" data-numeric>Amount</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">INV-1042</th>
        <td data-numeric>€1,250.00</td>
      </tr>
    </tbody>
  </table>
</aihio-table>
```

### A header that sorts on click cannot be reached with the keyboard, and never says which way it sorts. data-sortable gives the header a real button, sets aria-sort, and announces the new order.

Don't (aihio lint: table-click-handler):

```html
<aihio-table>
  <table>
    <caption>Invoices</caption>
    <thead>
      <tr>
        <th scope="col" onclick="sortBy('invoice')">Invoice</th>
        <th scope="col" onclick="sortBy('amount')" data-numeric>Amount</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">INV-1042</th>
        <td data-numeric>€1,250.00</td>
      </tr>
    </tbody>
  </table>
</aihio-table>
```

Do:

```html
<aihio-table>
  <table>
    <caption>Invoices</caption>
    <thead>
      <tr>
        <th scope="col" data-sortable="invoice">Invoice</th>
        <th scope="col" data-sortable="amount" data-numeric>Amount</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">INV-1042</th>
        <td data-numeric>€1,250.00</td>
      </tr>
    </tbody>
  </table>
</aihio-table>
```

### A row that opens on click is not a link. The keyboard cannot reach it, a screen reader does not announce it, and it cannot be opened in a new tab. Link the cell that names the row.

Don't (aihio lint: table-click-handler):

```html
<aihio-table>
  <table>
    <caption>Invoices</caption>
    <thead>
      <tr>
        <th scope="col">Invoice</th>
        <th scope="col" data-numeric>Amount</th>
      </tr>
    </thead>
    <tbody>
      <tr onclick="location.href = '/invoices/1042'">
        <th scope="row">INV-1042</th>
        <td data-numeric>€1,250.00</td>
      </tr>
    </tbody>
  </table>
</aihio-table>
```

Do:

```html
<aihio-table>
  <table>
    <caption>Invoices</caption>
    <thead>
      <tr>
        <th scope="col">Invoice</th>
        <th scope="col" data-numeric>Amount</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row"><a href="/invoices/1042">INV-1042</a></th>
        <td data-numeric>€1,250.00</td>
      </tr>
    </tbody>
  </table>
</aihio-table>
```

### There are no row or cell components. aihio-table wraps a native <table>, whose rows and cells are what a screen reader navigates and what the HTML parser keeps in order.

Don't (aihio lint: unknown-component):

```html
<aihio-table>
  <aihio-table-header>
    <aihio-table-row>
      <aihio-table-head>Invoice</aihio-table-head>
    </aihio-table-row>
  </aihio-table-header>
  <aihio-table-body>
    <aihio-table-row>
      <aihio-table-cell>INV-1042</aihio-table-cell>
    </aihio-table-row>
  </aihio-table-body>
</aihio-table>
```

Do:

```html
<aihio-table>
  <table>
    <caption>Invoices</caption>
    <thead>
      <tr>
        <th scope="col">Invoice</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>INV-1042</td>
      </tr>
    </tbody>
  </table>
</aihio-table>
```

### Nothing reads a sortable attribute on a <th>. The table reads data-sortable, and makes that header a sort button.

Don't (aihio lint: table-sortable-header):

```html
<aihio-table>
  <table>
    <caption>Invoices</caption>
    <thead>
      <tr>
        <th scope="col" sortable>Invoice</th>
        <th scope="col" sortable data-numeric>Amount</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">INV-1042</th>
        <td data-numeric>€1,250.00</td>
      </tr>
    </tbody>
  </table>
</aihio-table>
```

Do:

```html
<aihio-table>
  <table>
    <caption>Invoices</caption>
    <thead>
      <tr>
        <th scope="col" data-sortable>Invoice</th>
        <th scope="col" data-sortable data-numeric>Amount</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">INV-1042</th>
        <td data-numeric>€1,250.00</td>
      </tr>
    </tbody>
  </table>
</aihio-table>
```
