aria-colcount and aria-rowcount for Partially Rendered Grids
Permalink to "aria-colcount and aria-rowcount for Partially Rendered Grids"A virtualized grid renders twenty rows of ten thousand, and the accessibility tree describes exactly what is in the DOM: a twenty-row table. A grid with hidden columns describes four columns where the schema has seven. In both cases the user is told the window is the whole data set. The four counting attributes exist to correct that, and all four are frequently set from the wrong source — the DOM, rather than the data.
This page covers what each attribute must describe, the -1 case for genuinely unknown totals, and the three events that invalidate an index. It extends composite widget roles and states, and it is the counting half of accessible virtualized list patterns.
Spec reference
Permalink to "Spec reference"aria-rowcount and aria-colcount belong on the element with role="grid", role="table" or role="treegrid". Each states the total number of rows or columns in the full data set. The value -1 is defined to mean the total is unknown, and it is the correct value for a cursor-based feed with no count.
aria-rowindex and aria-colindex belong on rows and cells respectively, and state the 1-based absolute position in the full set. When the DOM contains every row and column, the browser computes these itself and setting them adds nothing. When the DOM contains a subset, they are the only way for the reader to report a real position.
There is one important subtlety: the counts describe the data set the user can currently reach, not the data set before filtering. A filter that narrows ten thousand rows to four hundred should leave aria-rowcount="400", because four hundred is what the user can now scroll through.
When to use — and when not to
Permalink to "When to use — and when not to"Set all four whenever any part of the grid is not in the DOM: vertical virtualization, horizontal virtualization, pagination with a known total, or hidden columns.
Do not set them on a fully rendered table. A hand-maintained aria-rowcount that disagrees with the DOM is worse than none, and it will disagree eventually.
Do not set aria-rowindex to the position within the window. It is the single most common implementation error in this area and produces “row 1 of 10,000” for the first visible row no matter where the user has scrolled to.
Annotated code example
Permalink to "Annotated code example"<!-- SC 1.3.1: the counts describe the DATA, the rows describe their absolute position -->
<table role="grid" aria-rowcount="10000" aria-colcount="7">
<thead>
<tr aria-rowindex="1">
<th role="columnheader" aria-colindex="1" scope="col">Account</th>
<th role="columnheader" aria-colindex="4" scope="col">Amount</th>
<!-- colindex 2 and 3 are hidden columns — the numbering keeps the schema honest -->
</tr>
</thead>
<tbody>
<!-- Only twenty rows are rendered; the indexes are absolute -->
<tr aria-rowindex="4821"> … </tr>
<tr aria-rowindex="4822"> … </tr>
</tbody>
</table>
// Indexes come from the data offset, never from the DOM position
function renderWindow(startIndex, rows) {
tbody.replaceChildren(...rows.map((row, i) => {
const tr = renderRow(row);
// +2 because the header row is aria-rowindex 1
tr.setAttribute('aria-rowindex', String(startIndex + i + 2));
return tr;
}));
}
// The counts change with the FILTER, not with the window
function onFilterApplied(matchingTotal) {
grid.setAttribute('aria-rowcount', String(matchingTotal));
announce(`${matchingTotal} rows match the current filters.`);
}
// Unknown totals are -1, not 0 and not the rendered count
function onStreamStarted() {
grid.setAttribute('aria-rowcount', '-1');
}
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Assistive technology | Reports position | Deviation |
|---|---|---|
| NVDA + Firefox | “row 4,821 of 10,000” on entering a row | Reads the count again after a large jump |
| JAWS + Chrome | Reports row and column position per cell | Verbose in a wide grid; users often turn it down |
| VoiceOver + Safari | Reports position in table mode | May not re-read the count until the table is re-entered |
| TalkBack + Chrome | Does not announce aria-rowindex |
Position must be carried in the cell name if it matters on touch |
The TalkBack row is a real constraint rather than a bug to work around: on touch, position information has to live in the accessible name of the cell if the user needs it, as described in how TalkBack differs for data grid navigation.
Integration context
Permalink to "Integration context"The counts are the contract between the data layer and the accessibility layer, which makes them a natural design system responsibility. A grid component that accepts totalRows and windowStart can derive every one of the four attributes itself, and a product that forgets to pass totalRows fails loudly rather than silently reporting the window.
They also interact with expansion. In a treegrid, aria-rowcount is the number of rows currently reachable at the present expansion state, so it changes on every expand and collapse — not the number of nodes in the underlying tree.
Gotchas
Permalink to "Gotchas"Header rows are not counted separately. aria-rowindex is 1-based across the whole grid including the header row, so the first body row in a table with one header row is index 2.
Counts frozen at mount. Filters, expansions and streams all change them. Recompute rather than setting once.
Zero instead of minus one. aria-rowcount="0" states there are no rows. Unknown is -1.
colindex assigned sequentially over rendered cells. With hidden columns, the rendered cells are not consecutive. Derive aria-colindex from the schema, not from the loop counter.
FAQ
Permalink to "FAQ"Should aria-rowcount count filtered-out rows?
No. It should state the number of rows currently in the result set — what the user could reach by scrolling or paging with the current filters applied. Counting rows the filters have excluded tells the user the grid contains data they cannot get to. Update it whenever the filter changes, and announce the new total in the same message that describes the filter.
When is -1 the right value for aria-rowcount?
When the total is genuinely unknown — a cursor-based endpoint with no count, or a stream still arriving. Minus one is defined as “unknown” and readers phrase it accordingly rather than announcing a wrong number. Replace it with the real total as soon as you learn it.
Do I need aria-colindex if no columns are hidden?
No. When every column is present in the DOM, the browser derives positions correctly and aria-colindex adds nothing. It becomes necessary the moment columns can be hidden or horizontally virtualized, because then the position in the DOM no longer matches the position in the schema.
Related
Permalink to "Related"- Composite widget roles and states — the parent guide and the full state reference
- Accessible virtualized list patterns — where these counts matter most
- Using aria-activedescendant for grid cell focus — the focus model that pairs with these counts