aria-rowindex
Defines an element's row index or position with respect to the total number of rows within a table, grid, or treegrid.
Overview
The aria-rowindex attribute defines a row's position within a table, grid, or treegrid. The value is a 1-based integer indicating the row's position in the complete dataset.
This attribute is essential for virtual scrolling implementations where only visible rows are rendered. It tells assistive technologies the true position of each row, enabling users to understand their location in large datasets.
You can set aria-rowindex on the row element itself (preferred) or on individual cells within the row.
Live Demo: Virtual Row Navigation
Each row has aria-rowindex indicating its position. Screen readers announce "Row 3 of 51" as users navigate, helping them understand their position in the full dataset.
Code Examples
Basic Usage
<!-- aria-rowindex indicates a row's position in the grid -->
<!-- Sparse table showing rows 1, 5, 10, and 15 of 20 -->
<table role="grid" aria-rowcount="20" aria-label="Quarterly reports">
<thead>
<tr aria-rowindex="1">
<th>Quarter</th>
<th>Revenue</th>
<th>Growth</th>
</tr>
</thead>
<tbody>
<tr aria-rowindex="5">
<td>Q1 2023</td>
<td>$1.2M</td>
<td>+5%</td>
</tr>
<tr aria-rowindex="10">
<td>Q2 2023</td>
<td>$1.4M</td>
<td>+8%</td>
</tr>
<tr aria-rowindex="15">
<td>Q3 2023</td>
<td>$1.6M</td>
<td>+12%</td>
</tr>
</tbody>
</table>
<!-- Screen reader: "Row 5 of 20, Q1 2023..." -->Virtual Scrolling
<!-- Virtual scrolling list with aria-rowindex -->
<div
role="grid"
aria-rowcount="1000"
aria-label="Message list"
>
<!-- Only render visible rows in viewport -->
<div role="row" aria-rowindex="245">
<div role="gridcell">Message from Alice</div>
<div role="gridcell">2024-01-15 10:30</div>
</div>
<div role="row" aria-rowindex="246">
<div role="gridcell">Reply from Bob</div>
<div role="gridcell">2024-01-15 10:35</div>
</div>
<div role="row" aria-rowindex="247">
<div role="gridcell">Message from Carol</div>
<div role="gridcell">2024-01-15 10:40</div>
</div>
<!-- ... more visible rows ... -->
</div>Row vs Cell Placement
<!-- aria-rowindex on row element (preferred) -->
<div role="grid" aria-rowcount="100">
<!-- rowindex on the row element -->
<div role="row" aria-rowindex="25">
<div role="gridcell">Cell A25</div>
<div role="gridcell">Cell B25</div>
<div role="gridcell">Cell C25</div>
</div>
</div>
<!-- Alternative: aria-rowindex on each cell -->
<div role="grid" aria-rowcount="100">
<div role="row">
<div role="gridcell" aria-rowindex="25">Cell A25</div>
<div role="gridcell" aria-rowindex="25">Cell B25</div>
<div role="gridcell" aria-rowindex="25">Cell C25</div>
</div>
</div>
<!-- Note: Prefer setting on row element for cleaner markup -->React Component
// React virtual grid with aria-rowindex
import { useState, useCallback, useRef } from 'react';
interface GridRow {
id: string;
data: string[];
}
interface VirtualGridProps {
rows: GridRow[];
totalRows: number;
rowHeight: number;
containerHeight: number;
columns: string[];
label: string;
}
function VirtualGrid({
rows,
totalRows,
rowHeight,
containerHeight,
columns,
label
}: VirtualGridProps) {
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
// Calculate visible range
const visibleRowCount = Math.ceil(containerHeight / rowHeight) + 1;
const startIndex = Math.floor(scrollTop / rowHeight);
const endIndex = Math.min(startIndex + visibleRowCount, rows.length);
const visibleRows = rows.slice(startIndex, endIndex);
const handleScroll = useCallback((e: React.UIEvent) => {
setScrollTop(e.currentTarget.scrollTop);
}, []);
// Handle keyboard navigation
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
const container = containerRef.current;
if (!container) return;
const activeRow = document.activeElement?.closest('[role="row"]');
if (!activeRow) return;
const currentIndex = parseInt(
activeRow.getAttribute('aria-rowindex') || '1'
);
switch (e.key) {
case 'ArrowDown':
// Navigate to next row
const nextIndex = Math.min(currentIndex + 1, totalRows);
// Scroll if needed and focus next row
e.preventDefault();
break;
case 'ArrowUp':
// Navigate to previous row
const prevIndex = Math.max(currentIndex - 1, 1);
e.preventDefault();
break;
case 'Home':
if (e.ctrlKey) {
// Go to first row
setScrollTop(0);
e.preventDefault();
}
break;
case 'End':
if (e.ctrlKey) {
// Go to last row
setScrollTop((totalRows - visibleRowCount) * rowHeight);
e.preventDefault();
}
break;
}
}, [totalRows, rowHeight, visibleRowCount]);
return (
<div
ref={containerRef}
role="grid"
aria-rowcount={totalRows + 1} // +1 for header
aria-label={label}
style={{ height: containerHeight, overflowY: 'auto' }}
onScroll={handleScroll}
onKeyDown={handleKeyDown}
>
{/* Header row */}
<div role="row" aria-rowindex={1} style={{ height: rowHeight }}>
{columns.map((col, i) => (
<div key={i} role="columnheader">
{col}
</div>
))}
</div>
{/* Spacer above visible rows */}
<div style={{ height: startIndex * rowHeight }} />
{/* Visible rows */}
{visibleRows.map((row, idx) => {
const actualRowIndex = startIndex + idx + 2; // +2 for header and 1-based
return (
<div
key={row.id}
role="row"
aria-rowindex={actualRowIndex}
style={{ height: rowHeight }}
tabIndex={0}
>
{row.data.map((cell, cellIdx) => (
<div key={cellIdx} role="gridcell">
{cell}
</div>
))}
</div>
);
})}
{/* Spacer below visible rows */}
<div style={{ height: (rows.length - endIndex) * rowHeight }} />
</div>
);
}
