Skip to content
Aihio v2.3.0
Search
GitHub
Search Describe what you are building, or name a component, intent, or token. Components and patterns are ranked the way the MCP server's find tool ranks them for an agent.

<aihio-data-grid> v1.0.0 Markdown for agents

Data Grid

A virtualized data table for large datasets, more rows than a page can hold (thousands to hundreds of thousands): one scrolling box in which only the rows in view are rendered, by your code, when the grid asks for them. Arrow keys move between cells as in a spreadsheet, and the grid tells assistive technology how many rows there are and which ones it shows. Find in page, printing, and reading the rows in a screen reader's browse mode only reach the rendered rows; when people need those, use aihio-table with aihio-pagination instead.

Intents tabular-data

Examples

Each one rendered live, over the markup that produces it.

A hundred thousand rows

The rows live in script. aihio-range asks for the ones in view and the script renders them into body; aihio-sort asks for them in a new order. Try the arrow keys, Page Down, and Control+End.

Request Method Path Status Duration (ms)
Show code · 48 lines
<aihio-data-grid id="requests-grid" row-count="100000" density="compact">
  <table aria-label="Requests">
    <colgroup>
      <col style="width: 8rem">
      <col style="width: 6rem">
      <col>
      <col style="width: 6rem">
      <col style="width: 9rem">
    </colgroup>
    <thead>
      <tr>
        <th scope="col" data-sortable="id" aria-sort="ascending">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>
<script type="module">
  const grid = document.getElementById('requests-grid');
  const methods = ['GET', 'POST', 'PATCH', 'DELETE'];
  const rows = Array.from({ length: 100000 }, (_, index) => ({
    id: index + 1,
    method: methods[index % 4],
    path: `/api/items/${(index * 7919) % 100000}`,
    status: [200, 201, 204, 404, 500][index % 5],
    duration: (index * 37) % 1500,
  }));

  function render(start, end) {
    grid.body.replaceChildren(...rows.slice(start, end).map((row) => {
      const tr = document.createElement('tr');
      tr.innerHTML = `<th scope="row">#${row.id}</th><td>${row.method}</td><td>${row.path}</td><td data-numeric>${row.status}</td><td data-numeric>${row.duration}</td>`;
      return tr;
    }));
  }

  grid.addEventListener('aihio-range', (event) => render(event.detail.start, event.detail.end));
  grid.addEventListener('aihio-sort', (event) => {
    const { column, direction } = event.detail;
    const order = direction === 'ascending' ? 1 : -1;
    rows.sort((a, b) => (a[column] > b[column] ? 1 : a[column] < b[column] ? -1 : 0) * order);
  });
  render(grid.start, grid.end);
</script>

Rows from a server

Fetch the rows asked for, and mark the grid loading while they come. A response for rows no longer in view is dropped: the person has scrolled on.

Order Customer Total
Show code · 44 lines
<aihio-data-grid id="orders-grid" row-count="25000">
  <table aria-label="Orders">
    <colgroup>
      <col style="width: 8rem">
      <col>
      <col style="width: 9rem">
    </colgroup>
    <thead>
      <tr>
        <th scope="col">Order</th>
        <th scope="col">Customer</th>
        <th scope="col" data-numeric>Total</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>
<script type="module">
  const grid = document.getElementById('orders-grid');

  // Stands in for fetch('/api/orders?start=...&end=...').
  const fetchOrders = (start, end) => new Promise((resolve) => setTimeout(() => resolve(
    Array.from({ length: end - start }, (_, offset) => ({
      id: 70000 + start + offset,
      customer: `Customer ${((start + offset) * 31) % 997}`,
      total: (((start + offset) * 7349) % 100000) / 100,
    }))
  ), 150));

  async function load(start, end) {
    grid.setAttribute('loading', '');
    const orders = await fetchOrders(start, end);
    if (start !== grid.start || end !== grid.end) return;
    grid.body.replaceChildren(...orders.map((order) => {
      const tr = document.createElement('tr');
      tr.innerHTML = `<th scope="row">#${order.id}</th><td>${order.customer}</td><td data-numeric>€${order.total.toFixed(2)}</td>`;
      return tr;
    }));
    grid.removeAttribute('loading');
  }

  grid.addEventListener('aihio-range', (event) => load(event.detail.start, event.detail.end));
  load(grid.start, grid.end);
</script>

Mistakes

Each one beside its fix, with what aihio lint says about it. The prompt fragment gives agents the same pairs, and the build fails if the linter stops catching one or a fix stops passing.

Without row-count the grid does not know how many rows there are, so it asks for none and its scroll box has no height to scroll.

Don't

<aihio-data-grid>
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable="id">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>

aihio lint reports

  • data-grid-row-count error Set row-count to how many rows there are in all. The grid asks for rows by number, and sizes its scroll box from the count.

Do

Request Method Path Status Duration (ms)
<aihio-data-grid row-count="100000">
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable="id">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>

An unnamed grid is announced only as "grid", which says nothing about what its hundred thousand rows are.

Don't

<aihio-data-grid row-count="100000">
  <table>
    <thead>
      <tr>
        <th scope="col" data-sortable="id">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>

aihio lint reports

  • table-accessible-name error Name the table: a <caption>, aria-labelledby pointing at the visible heading above it, or aria-label.

Do

Request Method Path Status Duration (ms)
<aihio-data-grid row-count="100000">
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable="id">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>

aihio-table renders every row it is given, so it has no row count.

A table of more rows than a page can hold is aihio-data-grid, or aihio-table with aihio-pagination.

Don't

<aihio-table row-count="100000">
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable="id">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-table>

aihio lint reports

  • unknown-attribute error <aihio-table> has no attribute "row-count". It accepts: density, sticky-header, manual-sort, loading, sort-ascending-text, sort-descending-text.

Do

Request Method Path Status Duration (ms)
<aihio-data-grid row-count="100000">
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable="id">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>

Without a column name aihio-sort reports the header's text, which changes when the page is translated.

Don't

<aihio-data-grid row-count="100000">
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable>Request</th>
        <th scope="col" data-sortable>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>

aihio lint reports

  • table-sort-column-name warn Give each sortable header a name, data-sortable="duration", so aihio-sort says which column to sort by without depending on the header's wording.

Do

Request Duration (ms)
<aihio-data-grid row-count="100000">
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable="id">Request</th>
        <th scope="col" data-sortable="duration">Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>

Firefox stops a box's height at 17.9 million pixels, about 389,000 rows of the default height.

The scroll ends there, before the rows do, and neither scrolling nor the keyboard reaches the rest.

Don't

<aihio-data-grid row-count="2000000">
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable="id">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>

aihio lint reports

  • data-grid-row-limit warn Narrow the rows with a filter, or show them a page at a time with aihio-table and aihio-pagination. Firefox stops a box's height at 17.9 million pixels, about 389,000 rows of the default height, and rows past it cannot be scrolled to.

Do

Request Method Path Status Duration (ms)
#1GET/api/items/020012
<aihio-table manual-sort>
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable="id" aria-sort="ascending">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody>
      <tr><th scope="row">#1</th><td>GET</td><td>/api/items/0</td><td data-numeric>200</td><td data-numeric>12</td></tr>
    </tbody>
  </table>
</aihio-table>
<aihio-pagination page="1" pages="80000" href="/requests?page={page}" aria-label="Request pages"></aihio-pagination>

API

Every attribute, property, method, and event the schema declares for aihio-data-grid.

Attributes

row-count
number
How many rows there are in all. Setting it, as when a filter changes the rows, asks for the rows in view again.
density
one of defaultcompact default default
Row height. compact fits more rows in the box.
loading
boolean default false
Marks the rows as stale while new ones load: they are dimmed, and the table has aria-busy.
sort-ascending-text
string default Sorted by {column}, ascending
Announced after sorting a column ascending, with {column} for the header's text. For localisation.
sort-descending-text
string default Sorted by {column}, descending
Announced after sorting a column descending, with {column} for the header's text.

Properties

table
HTMLTableElement | null read-only
The <table> the element enhances.
body
HTMLTableSectionElement | null read-only
The <tbody> your code renders the rows into. The grid keeps a section of its own above and below it, so table.tBodies[0] is not it.
start
number read-only
The first row asked for, counting from 0.
end
number read-only
The row after the last one asked for.
rowCount
number
Get or set row-count.

Methods

scrollToRow(index: number): void
Scrolls so the row at index, counting from 0, is at the top of the view, and asks for the rows there.

Events

aihio-range
detail { start: number, end: number }
Asks for rows: render the rows from start up to (not including) end into body, in order, replacing those there. Fired when the grid connects, when its rows in view change, when row-count is set, and after a sort. Rows fetched for a range that is no longer start to end should be dropped.
aihio-sort
detail { column: string, direction: 'ascending' | 'descending' }
Fired when the person sorts by a column, once its header carries the new aria-sort. Sort your rows by column; the grid then scrolls to the top and asks for the rows again with aihio-range.

Native elements

aihio-data-grid enhances these elements rather than replacing them, and reads these attributes on them. The linter checks them like its own.

<th>

A header cell. The grid uses its one header row for column headers, and keeps it in view.

Attributes
data-sortable
string
Makes the column sortable: the header's content becomes a button, and a click fires aihio-sort for your code to sort the rows by this name.
aria-sort
one of ascendingdescendingnoneother
The order your rows are in. The grid moves it from header to header as people sort.
data-numeric
boolean default false
Lines a column of figures up by place value. Put it on the header and on each of the column's cells.

<td>

A data cell, one line high: text that does not fit ends in an ellipsis.

Attributes
data-numeric
boolean default false
Lines the figure up with the rest of its column.

Composition

Required children
table
Allowed children
table

Accessibility

What you have to provide, and what the component already does. The obligations with a rule are checked by aihio lint and by the dev build's console warnings.

Your obligations

  • error When row-count is missing or not a whole number

    Set row-count to how many rows there are in all. The grid asks for rows by number, and sizes its scroll box from the count.

    Checked as data-grid-row-count

  • warn When row-count is above 350,000

    Narrow the rows with a filter, or show them a page at a time with aihio-table and aihio-pagination. Firefox stops a box's height at 17.9 million pixels, about 389,000 rows of the default height, and rows past it cannot be scrolled to.

    Checked as data-grid-row-limit

  • error When the table has no caption, aria-label, or aria-labelledby

    Name the table: a <caption>, aria-labelledby pointing at the visible heading above it, or aria-label.

    Checked as table-accessible-name

  • error When the table has no <th> cells

    Name each column with a <th> in the <thead> row.

    Checked as table-header-cells

  • warn When a sortable header has no column name

    Give each sortable header a name, data-sortable="duration", so aihio-sort says which column to sort by without depending on the header's wording.

    Checked as table-sort-column-name

  • warn When people need to find text in the page, print every row, or read the rows in a screen reader's browse mode

    Use aihio-table with aihio-pagination instead. A grid has only the rows in view in the page, and those are all that find in page, printing, and browse mode reach.

    Not machine-checkable: a judgment for the author.

Handled for you

  • The native <table> becomes a grid with aria-rowcount for every row there is, and each rendered row carries aria-rowindex, so a screen reader says "row 51,034 of 100,001" while only the rows in view exist
  • The grid is one tab stop. Arrow keys move between cells, Page Up and Page Down by the rows in view, Home and End to the ends of a row, and Control+Home and Control+End to the first and last cell
  • Moving to a row that is not rendered scrolls it into view, waits for your code to render it, and then focuses it
  • A cell holding one link or button is focused on that control; a cell holding several is entered with Enter or F2 and left with Escape. Every other control in the grid is out of the tab order
  • Keys pressed in a text field or an open menu inside a cell are left to it
  • If the row holding focus scrolls away and your code removes it, focus moves to the header of the same column rather than to the page
  • The header stays in view, and sorts the way aihio-table's does: a button in the <th>, aria-sort on the <th>, and the new order announced in a polite status region
  • Columns take their widths from <col> elements or the header cells, so they do not change as rows come and go, and each row is one line high
  • Cells draw their focus ring inside their edge, where the scroll box cannot clip it
  • loading sets aria-busy on the table while the rows are stale