Lattice Grid Buy a licence

demo D117

Screen reader in htmx

What the grid announces, and the ARIA it writes

aria-rowindex · aria-selected

This grid shows what a screen reader announces and the ARIA it writes for a virtualised table whose DOM holds only the visible cells. It gives assistive technology correct row and column counts and state, so a grid that recycles rows still reads accurately.

Building…
Loading a live grid…

This is the htmx version. A server-rendered table is upgraded in place, so the page is readable before any script runs and gains the full grid when the module loads, which suits a progressively enhanced app.

The configuration

<link rel="stylesheet" href="https://cdn.latticegrid.dev/lattice-grid@1.107.0/lattice-grid.min.css">

<!-- A real, server-rendered table: readable with JavaScript off. data-field on
     each header names the column that cell becomes, and data-id on each row is
     the key the grid carries it under. -->
<table id="screen-reader-table" data-lattice-grid>
  <thead>
    <tr>
      <th data-field="service">Service</th>
      <th data-field="team">Team</th>
      <th data-field="state">State</th>
      <th data-field="instances">Instances</th>
      <th data-field="uptime">Uptime</th>
    </tr>
  </thead>
  <tbody>
    <tr data-id="1"><td>svc-00001</td><td>Search</td><td>unknown</td><td>49</td><td>0.9523</td></tr>
    <tr data-id="2"><td>svc-00002</td><td>Mobile</td><td>degraded</td><td>162</td><td>0.9871</td></tr>
    <tr data-id="3"><td>svc-00003</td><td>Identity</td><td>degraded</td><td>20</td><td>0.9895</td></tr>
    <!-- the rest of the rows, rendered by your server -->
  </tbody>
</table>

<script type="module">
  import { hydrateTable } from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/modules/htmx.esm.min.js';

  // The markup carries the rows; a server-rendered table's column types are
  // declared in the config hydrateTable takes. The grid replaces the table
  // where it stood.
  const grid = hydrateTable(document.getElementById('screen-reader-table'), {
    selection: 'multiple',
    columns: [
      { field: 'service', title: 'Service', layout: { width: 150 } },
      { field: 'team', title: 'Team', filter: { type: 'set' } },
      { field: 'state', title: 'State' },
      { field: 'instances', title: 'Instances', type: 'number' },
      { field: 'uptime', title: 'Uptime', type: 'number', format: { style: 'percent', decimals: 3 } },
    ],
  });

  // Selecting through the API sets aria-selected on those rows, so assistive
  // technology announces them as chosen.
  const keys = [1, 2].map((i) => grid.rows.get(i)?.key).filter(Boolean);
  if (keys.length) grid.selection.set(keys);
</script>

What a screen reader hears when it lands on a grid

A JavaScript data grid that virtualises its rows presents a problem no static table has: the DOM only contains the cells currently on screen, so a screen reader’s row and column counts have to come from attributes, not from counting children. Lattice Grid writes aria-rowindex and aria-colindex on every rendered cell against the full logical size of the data, not the rendered window, so a user hears “row 4,208 of 1,000,000” even though row 4,208 is the only one of those in the DOM at that moment. Selection state is exposed the same way: aria-selected tracks the grid’s own selection model on the row and, where relevant, the cell, so a checkbox column and a screen reader announcement never drift apart. A developer reaches for this when a procurement checklist names WCAG or Section 508, or when support tickets show a user navigating with NVDA, JAWS or VoiceOver rather than a mouse. The grid role structure follows the ARIA grid pattern, with aria-rowcount and aria-colcount set on the container so assistive technology can announce position without walking the whole dataset, and it holds regardless of column virtualisation, so a wide grid with two hundred columns still reports the same indices for the ones currently off-screen.

Does a virtualised JavaScript data grid work with a screen reader?

It can, provided the rows and columns still on screen carry position information about the full dataset, not just the rendered subset. Lattice Grid sets aria-rowindex, aria-colindex, aria-rowcount and aria-colcount from the logical grid size, and keeps aria-selected in step with its selection model, so announcements stay correct as the DOM recycles during scrolling.