Virtualized Divs, Sortable Headers, and a Grid Proposal
A logistics company's internal shipment dashboard renders 1,200 rows in a div-based table, virtualized so only about 40 rows exist in the DOM at once. Headers are clickable divs. After a screen-reader complaint, the team added role="table", role="row" and role="cell", but sorting is still announced inconsistently, and a tester reports that after filtering, NVDA says "row 5 of 0." Expanding a shipment inserts detail content into the same row's last cell, pushing the layout sideways, and focus is lost. The team proposes adding role="grid" and more ARIA, arguing that native tables "can't be virtualized" and that a grid is "more modern." One developer suggests putting aria-live on the whole table container to announce changes. Three sprints remain; 400 warehouse coordinators use the dashboard daily, and several use JAWS.
1. Should this be a native table, an ARIA table, or a grid? Justify the choice against how the widget actually behaves.
Read the worked answer
Start from behavior, not from the role name. Users read and sort data; they do not edit cells in place. That is tabular data with controls in headers, which a native <table> handles well: header cells are associated automatically, screen readers announce column and row context, and sort buttons placed inside <th> keep the semantics while giving keyboard operation. Add aria-sort to the sorted column header only (ascending or descending), and update it when sorting changes.
A grid is the wrong default. role="grid" signals a composite widget with an internal focus model, typically one tab stop and arrow-key navigation between cells. If the team adopts it, they owe users a complete keyboard interaction model, consistent focus management, and documented keys - a large commitment that the current implementation does not meet. Declaring a grid while leaving every cell tabbable and every header a plain clickable div creates a worse mismatch than the current one.
Virtualization is not a reason to abandon table semantics. Native tables can be virtualized; the parts that need care are counts and indices, which ARIA properties handle. If the DOM structure cannot be a real <table> because of the rendering approach, an ARIA table with correct roles is acceptable - but native first, ARIA only for the gap.
2. Explain the "row 5 of 0" announcement and the focus loss, and specify the fixes for virtualization and row expansion.
Read the worked answer
The "of 0" comes from the accessibility tree reflecting only the rows currently in the DOM. The browser and screen reader compute the row count from what exists, so a virtualized table that shows 40 of 1,200 rows announces a total of 40 - or zero when a filter temporarily removes rows before re-rendering. Set aria-rowcount on the table to the true total (1,200, or the filtered total) and aria-rowindex on each rendered row to its position in the full set, so "row 5 of 1,200" is correct even though only 40 rows exist.
For focus loss on expansion: the expanded content should not be injected into the last cell of an existing row while the trigger is being re-rendered. Put a disclosure button in the first cell with aria-expanded and aria-controls, and insert the details as a new row spanning the table immediately after the source row. Keep focus on the button; do not move it into the new content. Announce only the change of state, not the entire table: a focused button whose aria-expanded flips to true is announced by the screen reader, so no live region is needed. A live region wrapped around the table would re-read large amounts of data on every keystroke and should be rejected.
3. The developer proposes aria-live on the table container to announce sorting and filtering. Assess this and describe what should be announced instead.
Read the worked answer
A live region on the table container is the wrong instrument. Live regions announce inserted or changed content according to their politeness setting; a container holding hundreds of cells will produce either silence (if the region is not correctly set up) or a flood of cell text on every sort. It also makes the announcement compete with the user's own navigation, so someone arrowing through rows hears stale status messages interleaved with data. aria-live="assertive" makes this worse by interrupting.
The information users actually need after an action is short and stable:
- After sorting: which column and direction, for example "Sorted by departure date, ascending."
aria-sorton the header conveys this when the user returns to the header, and a single polite status message covers the moment of change. - After filtering: the new result count, "Showing 132 shipments." One small, dedicated status element, updated once the filter settles.
- After expanding a row: nothing beyond the button's
aria-expandedstate.
Use one visually hidden status region with role="status" (polite), placed outside the table, and update its text rather than appending. Debounce filter announcements so rapid typing produces one message, not one per character.