Accessibility
Good accessibility isn't just compliance — it's building software that works for everyone. A keyboard user in a tight workflow shouldn't have to reach for the mouse. A screen reader user deserves the same experience as anyone else.
OGrid is built with this in mind. Keyboard navigation, screen reader support, high contrast mode, and ARIA semantics are all baked in — you don't configure them, you just benefit from them.
What's built in by default
You get all of this without any extra setup:
- Full keyboard navigation — arrow keys, Tab, Home/End, Ctrl+Arrow, all working like a spreadsheet
- Screen reader announcements for sort, filter, edit, selection, and pagination changes
- Proper ARIA roles and attributes on the grid, rows, cells, and headers
:focus-visibleindicators so keyboard users always know where they are- Focus trapping in dropdowns and popovers (Escape always gets you out)
- High contrast mode support via CSS custom properties with system color fallbacks
- Semantic HTML — real
<table>,<thead>,<th>,<td>elements that assistive tech understands
The one thing you do need to add: an aria-label on the grid. It's one prop.
<OGrid
data={products}
columns={columns}
getRowId={(item) => item.id}
aria-label="Product catalog"
/>
Keyboard navigation
OGrid uses Excel-style keyboard shortcuts, so if your users know spreadsheets, they already know how to navigate.
Moving around
| Key | Action |
|---|---|
↑ ↓ ← → | Move active cell |
Ctrl+↑ / Ctrl+↓ | Jump to first/last row in column |
Ctrl+← / Ctrl+→ | Jump to first/last column in row |
Home | First column in row |
End | Last column in row |
Ctrl+Home | Top-left cell |
Ctrl+End | Bottom-right cell |
Selecting cells
| Key | Action |
|---|---|
Shift+↑↓←→ | Extend selection |
Shift+Home / Shift+End | Extend selection to row edge |
Shift+Ctrl+Home / Shift+Ctrl+End | Extend to grid corner |
Ctrl+A | Select all cells |
Space (on checkbox column) | Toggle row selection |
Editing
| Key | Action |
|---|---|
Enter or F2 | Start editing the active cell |
Escape | Cancel edit, revert value |
Enter (while editing) | Commit and move down |
Tab / Shift+Tab | Commit and move right/left |
Everything else
| Key | Action |
|---|---|
Ctrl+C / Ctrl+X / Ctrl+V | Copy, cut, paste |
Delete | Clear selected cells |
Ctrl+Z / Ctrl+Y | Undo / redo |
Shift+F10 | Open context menu |
Enter / Space on column header | Toggle sort |
ARIA markup
The grid renders with full ARIA attributes so assistive technologies have the context they need.
<div role="grid" aria-label="Product catalog" aria-rowcount="1000">
<table>
<thead>
<tr role="row">
<th role="columnheader" aria-sort="ascending" scope="col">
Product Name
</th>
</tr>
</thead>
<tbody>
<tr role="row" aria-rowindex="1" aria-selected="false">
<td role="gridcell" tabindex="-1">Widget A</td>
</tr>
</tbody>
</table>
</div>
Key attributes and what they do:
| Attribute | Purpose |
|---|---|
role="grid" | Identifies the interactive grid container |
aria-label | Gives the grid an accessible name |
aria-rowcount | Total rows (useful for server-side pagination where not all rows are in the DOM) |
aria-rowindex | Row position in the full dataset |
aria-sort | Current sort state on column headers |
aria-selected | Whether a row is selected |
aria-expanded | Open/closed state of filter dropdowns |
aria-current="page" | Current page in pagination |
The status bar uses role="status" with aria-live="polite" — it announces changes without interrupting what the user is doing.
Screen reader support
OGrid announces meaningful state changes:
- Navigation: "Product Name, cell, row 1, column 2"
- Sort: "Sorted by Product Name, ascending"
- Edit: "Editing Salary, row 3"
- Value change: "Salary changed from $68,000 to $72,000"
- Pagination: "Showing 21 to 40 of 250 rows, page 2 of 13"
- Selection: "5 rows selected"
Tested with: NVDA and JAWS (Windows), VoiceOver (macOS), Narrator (Windows).
Providing extra context
For grids embedded in a page with other content, it helps to connect the grid to surrounding descriptive text:
<div>
<h2 id="orders-heading">Recent Orders</h2>
<p id="orders-description">
Your last 30 days of orders. Use arrow keys to navigate, Enter to edit.
</p>
<OGrid
data={orders}
columns={columns}
getRowId={(item) => item.id}
aria-labelledby="orders-heading"
aria-describedby="orders-description"
/>
</div>
If you need to announce dynamic state changes from outside the grid (selection counts, save confirmations), use an ARIA live region alongside it:
function GridWithAnnouncements() {
const [status, setStatus] = useState('');
return (
<>
<div role="status" aria-live="polite" className="sr-only">
{status}
</div>
<OGrid
data={orders}
columns={columns}
getRowId={(item) => item.id}
rowSelection="multiple"
onSelectionChange={({ selectedRowIds }) => {
const count = selectedRowIds.length;
setStatus(`${count} ${count === 1 ? 'order' : 'orders'} selected`);
}}
/>
</>
);
}
The .sr-only pattern keeps the announcement visually hidden but readable by assistive tech:
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
Automated accessibility testing
OGrid's test suite includes jest-axe accessibility tests. You can do the same in your own tests:
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { OGrid } from '@alaarab/ogrid-react-radix';
expect.extend(toHaveNoViolations);
test('grid has no accessibility violations', async () => {
const { container } = render(
<OGrid
data={products}
columns={columns}
getRowId={(item) => item.id}
aria-label="Product catalog"
/>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
npm install --save-dev jest-axe
Manual testing checklist
Automated tests catch structural issues but can't cover everything. Before shipping:
- Navigate the entire grid using only the keyboard — no mouse
- Enable VoiceOver (
Cmd+F5) or NVDA and navigate through sort, filter, edit - Test at 200% zoom — nothing should overflow or become unusable
- Toggle Windows High Contrast Mode — all content should remain visible
- Verify
aria-labelis set on the grid (one of the most common misses)
Known limitations
Virtual scrolling: When virtualScroll.enabled is true, only visible rows are in the DOM. Screen readers won't know the full dataset size unless you set aria-rowcount on the grid wrapper manually.
Custom cell renderers: If you use renderCell to render custom content inside cells, you're responsible for making that content accessible — ARIA attributes, keyboard interaction, announcements. The grid provides the structure; your custom content needs to carry its own accessibility.
Resources
- ARIA Grid Pattern — the spec OGrid follows
- WCAG 2.1 Quick Reference
- axe DevTools Browser Extension — free in-browser auditing
Found an accessibility issue? Open an issue and tag it accessibility with your browser, screen reader version, and reproduction steps.