Overview
React Data Grid is built with accessibility as a priority. It implements proper ARIA attributes and keyboard navigation following the ARIA Grid pattern and ARIA Treegrid pattern.
ARIA Attributes
Grid Role
The grid container has the role="grid" attribute by default:
import { DataGrid } from 'react-data-grid';
function MyGrid() {
return <DataGrid columns={columns} rows={rows} />;
}
For tree grids with hierarchical data:
import { TreeDataGrid } from 'react-data-grid';
function MyTreeGrid() {
return (
<TreeDataGrid
columns={columns}
rows={rows}
groupBy={['category']}
rowGrouper={rowGrouper}
expandedGroupIds={expandedGroupIds}
onExpandedGroupIdsChange={setExpandedGroupIds}
/>
);
}
TreeDataGrid automatically sets role="treegrid" and manages appropriate ARIA attributes for hierarchical navigation.
Grid Labels
Provide accessible labels using aria-label or aria-labelledby:
// Using aria-label
<DataGrid aria-label="Product inventory" columns={columns} rows={rows} />
// Using aria-labelledby
<>
<h2 id="grid-title">Product Inventory</h2>
<DataGrid aria-labelledby="grid-title" columns={columns} rows={rows} />
</>
Always provide a label using aria-label or aria-labelledby for screen reader users.
Grid Description
Add descriptions using aria-description or aria-describedby:
// Using aria-description
<DataGrid
aria-label="Product inventory"
aria-description="Use arrow keys to navigate, Enter to edit"
columns={columns}
rows={rows}
/>
// Using aria-describedby
<>
<p id="grid-desc">Use arrow keys to navigate, Enter to edit cells</p>
<DataGrid
aria-label="Product inventory"
aria-describedby="grid-desc"
columns={columns}
rows={rows}
/>
</>
Row Count
The grid automatically sets aria-rowcount to the total number of rows:
// Automatic: aria-rowcount = headerRows + dataRows + summaryRows
<DataGrid columns={columns} rows={rows} />
Override for virtualized or paginated data:
// Display page 1 of 100 pages, 10 rows per page
<DataGrid
aria-label="Products (Page 1 of 100)"
aria-rowcount={1000} // Total rows across all pages
columns={columns}
rows={currentPageRows} // 10 rows
/>
Column Count
Automatically set via aria-colcount:
// Automatic: aria-colcount = columns.length
<DataGrid columns={columns} rows={rows} />
Row Index
Each row has aria-rowindex (1-based):
// Automatically applied:
// Header row: aria-rowindex="1"
// First data row: aria-rowindex="2"
// Second data row: aria-rowindex="3"
Column Index
Each cell has aria-colindex (1-based):
// Automatically applied:
// First cell: aria-colindex="1"
// Second cell: aria-colindex="2"
Row Selection
Selected rows have aria-selected="true":
import { useState } from 'react';
import { DataGrid, SelectColumn, type Column } from 'react-data-grid';
interface Row {
id: number;
name: string;
}
const columns: readonly Column<Row>[] = [
SelectColumn,
{ key: 'id', name: 'ID' },
{ key: 'name', name: 'Name' }
];
function MyGrid() {
const [selectedRows, setSelectedRows] = useState((): ReadonlySet<number> => new Set());
return (
<DataGrid
aria-label="Users"
columns={columns}
rows={rows}
rowKeyGetter={(row) => row.id}
selectedRows={selectedRows}
onSelectedRowsChange={setSelectedRows}
/>
);
}
When row selection is enabled, the grid automatically sets aria-multiselectable="true".
Checkbox Labels
Provide labels for selection checkboxes:
import { DataGrid, renderCheckbox } from 'react-data-grid';
function MyGrid() {
return (
<DataGrid
columns={columns}
rows={rows}
renderers={{
renderCheckbox: (props) => renderCheckbox({ ...props, 'aria-label': 'Select row' })
}}
/>
);
}
Keyboard Navigation
Arrow Keys
Navigate between cells:
- Arrow Up - Move to cell above
- Arrow Down - Move to cell below
- Arrow Left - Move to cell on left
- Arrow Right - Move to cell on right
<DataGrid aria-label="Products" columns={columns} rows={rows} />
Tab Navigation
- Tab - Move to next cell
- Shift + Tab - Move to previous cell
When Tab reaches the last cell, focus moves to the next focusable element outside the grid.
Home and End Keys
- Home - Move to first cell in row
- Ctrl + Home - Move to first cell in grid
- End - Move to last cell in row
- Ctrl + End - Move to last cell in grid
Page Up and Page Down
- Page Up - Scroll up one viewport
- Page Down - Scroll down one viewport
Editing
- Enter - Enter edit mode for editable cells
- Escape - Cancel editing and discard changes
- Enter (in edit mode) - Commit changes and exit edit mode
Row Selection
- Space - Select/deselect current row (when SelectColumn is present)
- Shift + Space - Toggle row selection without changing focus
Custom Keyboard Behavior
Override default behavior with onCellKeyDown:
import type { CellKeyDownArgs, CellKeyboardEvent } from 'react-data-grid';
function onCellKeyDown(args: CellKeyDownArgs<Row>, event: CellKeyboardEvent) {
if (args.mode === 'SELECT' && event.key === 'Enter') {
// Prevent default "start editing" behavior
event.preventGridDefault();
// Custom action
console.log('Enter pressed on row:', args.row);
}
}
<DataGrid columns={columns} rows={rows} onCellKeyDown={onCellKeyDown} />;
Preventing Tab Exit
Prevent Tab from leaving the grid:
import type { CellKeyDownArgs, CellKeyboardEvent } from 'react-data-grid';
function onCellKeyDown(args: CellKeyDownArgs<Row>, event: CellKeyboardEvent) {
if (args.mode === 'SELECT' && event.key === 'Tab') {
event.preventGridDefault();
}
}
<DataGrid columns={columns} rows={rows} onCellKeyDown={onCellKeyDown} />;
TreeGrid Navigation
For hierarchical data, additional keyboard shortcuts apply:
- Arrow Right - Expand collapsed group (on group row)
- Arrow Left - Collapse expanded group (on group row)
- Arrow Left - Move to parent group (on child row)
import { TreeDataGrid } from 'react-data-grid';
function MyTreeGrid() {
return (
<TreeDataGrid
aria-label="Grouped products"
columns={columns}
rows={rows}
groupBy={['category', 'subcategory']}
rowGrouper={rowGrouper}
expandedGroupIds={expandedGroupIds}
onExpandedGroupIdsChange={setExpandedGroupIds}
/>
);
}
Group rows automatically receive appropriate ARIA attributes including aria-expanded, aria-level, aria-posinset, and aria-setsize.
Focus Management
Grid Focus
The grid container is keyboard focusable with tabIndex={-1}:
// Focus moves to the grid container first
<DataGrid aria-label="Products" columns={columns} rows={rows} />
Cell Focus
Active cells receive tabIndex={0} for keyboard focus:
// Only the selected cell is in the tab order
// All other cells have tabIndex={-1}
Programmatic Selection
Use the ref to programmatically select cells:
import { useRef } from 'react';
import { DataGrid, type DataGridHandle } from 'react-data-grid';
function MyGrid() {
const gridRef = useRef<DataGridHandle>(null);
function selectCell() {
gridRef.current?.selectCell({ idx: 0, rowIdx: 0 }, { shouldFocusCell: true });
}
return (
<>
<button onClick={selectCell}>Focus first cell</button>
<DataGrid ref={gridRef} aria-label="Products" columns={columns} rows={rows} />
</>
);
}
Screen Reader Support
Testing with Screen Readers
Test with multiple screen readers:
- NVDA (Windows) - Free and open source
- JAWS (Windows) - Industry standard
- VoiceOver (macOS/iOS) - Built-in
- TalkBack (Android) - Built-in
Announcements
Screen readers announce:
- Grid label and description
- Current cell position (row and column)
- Column headers
- Cell content
- Selection state
- Sort state
- Edit mode state
Example Announcement
<DataGrid
aria-label="Product inventory"
aria-description="Sortable data grid with 100 products"
columns={columns}
rows={rows}
/>
When navigating:
“Product inventory. Sortable data grid with 100 products. Column 1, Row 2. Product name: Laptop.”
Testing Accessibility
Querying by Role
Use role-based queries in tests:
import { render, screen } from '@testing-library/react';
import { DataGrid } from 'react-data-grid';
test('grid is accessible', () => {
render(<DataGrid aria-label="Products" columns={columns} rows={rows} />);
const grid = screen.getByRole('grid', { name: 'Products' });
expect(grid).toBeInTheDocument();
const rows = screen.getAllByRole('row');
expect(rows.length).toBeGreaterThan(0);
});
Axe Testing
Use axe-core for automated accessibility testing:
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';
import { DataGrid } from 'react-data-grid';
test('grid has no accessibility violations', async () => {
const { container } = render(
<DataGrid aria-label="Products" columns={columns} rows={rows} />
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Best Practices
Always provide:
aria-label or aria-labelledby for grid identification
- Proper column names for header cells
aria-label for icon buttons and checkboxes
- Keyboard navigation support
Test with keyboard only (no mouse) to ensure all functionality is accessible.
Column Headers
Provide clear, descriptive column names:
const columns: readonly Column<Row>[] = [
{ key: 'id', name: 'Product ID' }, // ✅ Clear
{ key: 'name', name: 'Product Name' }, // ✅ Clear
{ key: 'price', name: '$' } // ❌ Not descriptive
];
Label action buttons in custom cells:
function ActionCell({ row }: RenderCellProps<Row>) {
return (
<>
<button aria-label={`Edit ${row.name}`}>✏️</button>
<button aria-label={`Delete ${row.name}`}>🗑️</button>
</>
);
}
Loading States
Indicate loading with aria-busy:
function MyGrid() {
const [isLoading, setIsLoading] = useState(true);
return (
<div aria-busy={isLoading} aria-label="Products">
<DataGrid columns={columns} rows={rows} />
</div>
);
}
Resources