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.
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.