aria-rowindex and aria-colindex in Practice

Permalink to "aria-rowindex and aria-colindex in Practice"

When a grid renders only part of its data — virtualized rows, paged results, hidden columns — the DOM no longer tells assistive technology where a row sits. Row 5,001 in the data might be the third tr in the DOM. aria-rowindex and aria-colindex restore the true positions, so a screen reader can say “row 5,001 of 10,000”. They are simple attributes with a few sharp edges: they are 1-based, they count header rows, they must be consistent with aria-rowcount, and they are only needed when the DOM is incomplete.

This guide belongs to composite widget roles and states.

Spec reference

Permalink to "Spec reference"
  • aria-rowindex — 1-based index of a row within the full table, grid or treegrid; on rows, or on cells when rows are incomplete.
  • aria-colindex — 1-based index of a cell’s column within the full set of columns.
  • aria-rowcount / aria-colcount — total rows and columns, or −1 if unknown.
  • SC 1.3.1 (A) — position information must be programmatic when the DOM does not reflect it.
Rendered rows inside a larger data set Layers: the full data set has 10,000 rows plus one header row; the virtual window renders rows 5,000 to 5,020; each rendered row carries its aria-rowindex; the header row keeps index 1. Rendered rows inside a larger data setFull data setaria-rowcount="10001" (header + 10,000)Header rowaria-rowindex="1"Rendered windowaria-rowindex 5001 … 5021Off-screen rowsnot in the DOM
DOM order and data position differ; the attributes reconcile them.

When to use — and when not to

Permalink to "When to use — and when not to"

Use them when rows or columns are missing from the DOM: virtualization, server-side paging presented as one grid, or columns hidden for space but logically present.

Do not add them to complete, static tables; browsers compute positions automatically. The misapplication: 0-based indexes copied from the data array, which makes every row announce one lower than it is and the header row become row 0.

Common index mistakes versus correct indexing Comparison: zero-based indexes, forgetting header rows and per-page restarting all produce wrong positions; 1-based global indexes that include headers match the count. Common index mistakes versus correct indexing✗ 0-based from array indexHeader row not countedIndexes restart each pagearia-rowcount = data length✓ 1-based positionsHeader is row 1Global positionsData length + header rows

Annotated code example

Permalink to "Annotated code example"
<div role="grid" aria-label="Transactions" aria-rowcount="10001" aria-colcount="12">
  <div role="row" aria-rowindex="1">
    <div role="columnheader" aria-colindex="1">Date</div>
    <div role="columnheader" aria-colindex="2">Payee</div>
    <div role="columnheader" aria-colindex="7">Amount</div>        <!-- columns 3–6 hidden -->
  </div>
  <div role="row" aria-rowindex="5001">                            <!-- data row 5,000 + 1 header -->
    <div role="gridcell" aria-colindex="1">14 Mar</div>
    <div role="gridcell" aria-colindex="2">Lund &amp; Co</div>
    <div role="gridcell" aria-colindex="7">1,240.00</div>
  </div>
</div>
const HEADER_ROWS = 1;
function rowAttrs(dataIndex) {                 // dataIndex is 0-based in the array
  return { 'aria-rowindex': dataIndex + 1 + HEADER_ROWS };
}

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Screen reader On a cell in row 5,001, column 7
NVDA + Firefox “row 5001, column 7, Amount, 1,240.00”
JAWS + Chrome “Amount, row 5001, 1,240.00”
VoiceOver + Safari “1,240.00, row 5001 of 10001” (varies)
Ctrl+Home Moves to first row; app must render it
When each attribute is needed Matrix of grid situations — complete static table, virtualized rows, virtualized columns, paged grid presented as one, hidden columns — against the need for aria-rowcount, aria-rowindex, aria-colcount and aria-colindex. When each attribute is neededrowcountrowindexcolcountcolindexComplete static tableNoNoNoNoVirtualized rowsYesYesNoNoVirtualized columnsNoNoYesYesPaged as one gridYesYesNoNoHidden columnsNoNoYesYes
Add only what the DOM cannot express.

Step-by-step implementation

Permalink to "Step-by-step implementation"
  1. Decide the total. Set aria-rowcount to header rows plus data rows. If the total is unknown (infinite feed), use −1 and do not promise “of N”.
  2. Index every rendered row. Compute aria-rowindex from the data index plus header rows, never from the DOM position.
  3. Index columns only when needed. If all columns are rendered, skip aria-colindex. If some are virtualized or hidden, put it on every cell (or on rows with aria-colindex per cell where cells are missing).
  4. Keep sticky and pinned rows right. A pinned totals row at the bottom has the last index, even though it is rendered near the top of the DOM.
  5. Verify announcements. Navigate with each screen reader and confirm the spoken positions match visible row numbers.

Integration context

Permalink to "Integration context"

Counts are covered in aria-colcount and aria-rowcount for partial grids. Virtualization techniques are in accessible virtualized list patterns and two-dimensional virtualized grids. Inspecting these values in the accessibility tree is described in accessibility tree and API inspection. Cell context announcements are in announcing cell context on navigation.

Gotchas

Permalink to "Gotchas"

Native tables. aria-rowindex works on tr elements in native tables too, which is useful for virtualized native tables.

Sorting. After sorting, indexes describe position in the sorted order, not the original data order.

Mismatched counts. If aria-rowindex exceeds aria-rowcount, some screen readers stop announcing positions entirely.

Edge cases

Permalink to "Edge cases"
  • Grouped rows. In treegrids, group header rows count as rows and need indexes.
  • Rows with rowspan. Indexes refer to the starting row of a spanning cell.
  • Unknown totals that become known. Update aria-rowcount from −1 to the real value when the count arrives.

Worked example

Permalink to "Worked example"

A banking app’s virtualized transaction grid announced “row 3” for every row near the top of the viewport because positions came from the DOM. The team added aria-rowcount of data length plus one header row and computed aria-rowindex from the data index plus one. The first attempt was off by one (they forgot the header); a screen reader check caught it. Users could finally tell where they were in a ten-thousand-row history.

Common mistakes

Permalink to "Common mistakes"
  • 0-based indexes.
  • Not counting header rows.
  • Adding indexes to complete tables.
  • Counts inconsistent with indexes.

Testing checklist

Permalink to "Testing checklist"

Testing it with a screen reader

Permalink to "Testing it with a screen reader"

Scroll a virtualized grid to the middle and move focus into a row. With NVDA, press Ctrl+Alt+Down and listen for row numbers; they should match the visible row number plus header rows. Check the accessibility tree in Chrome DevTools to confirm aria-rowindex on the focused row. Press Ctrl+End and confirm the last row index equals aria-rowcount. Repeat after sorting. In VoiceOver, listen for “row X of Y” and confirm Y matches the count.

Debugging wrong positions

Permalink to "Debugging wrong positions"

When a screen reader announces the wrong row, check three things in order. First, the count: inspect the grid in the accessibility tree and confirm aria-rowcount includes header rows. Second, the index source: log the data index and computed aria-rowindex for the focused row and confirm the arithmetic. Third, recycling: virtualized libraries reuse row elements, and a stale attribute from a previous row can survive if the render path skips attribute updates. Setting the attribute in the same function that sets the row’s content avoids that class of bug.

Design system notes

Permalink to "Design system notes"

Have the grid component compute indexes and counts internally from data, so consumers never set them by hand.

Showing row numbers visually

Permalink to "Showing row numbers visually"

Screen reader users get positions from the attributes, but sighted users of large virtualized grids also lose track of where they are once the scrollbar thumb becomes a sliver. Consider a visible row-number column or a position indicator (“Rows 5,000–5,020 of 10,000”) near the grid. When you do, derive the visible numbers from the same function that computes aria-rowindex minus header rows, so the two never disagree. A visible indicator also helps support conversations: a user can say “row 5,014” and a colleague can find it with a “Go to row” command, which is worth adding to any grid large enough to need these attributes.

FAQ

Permalink to "FAQ"
Are the attributes required for virtualized grids?

Without them, positions are wrong; treat them as required.

Do header rows count?

Yes. The header row is row 1.

Can I put aria-rowindex on cells?

Yes, when rows are incomplete, but rows are the usual place.

What if the total is unknown?

Use aria-rowcount=“-1” and keep rowindex accurate.

Do screen readers announce colindex?

Most announce column numbers from it when present.

Does this help non-screen-reader users?

Indirectly, through consistent row numbers if you also show them visually.

Permalink to "Related"

← Back to Composite Widget Roles & States