Skip to main content

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-visible indicators 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

KeyAction
Move active cell
Ctrl+↑ / Ctrl+↓Jump to first/last row in column
Ctrl+← / Ctrl+→Jump to first/last column in row
HomeFirst column in row
EndLast column in row
Ctrl+HomeTop-left cell
Ctrl+EndBottom-right cell

Selecting cells

KeyAction
Shift+↑↓←→Extend selection
Shift+Home / Shift+EndExtend selection to row edge
Shift+Ctrl+Home / Shift+Ctrl+EndExtend to grid corner
Ctrl+ASelect all cells
Space (on checkbox column)Toggle row selection

Editing

KeyAction
Enter or F2Start editing the active cell
EscapeCancel edit, revert value
Enter (while editing)Commit and move down
Tab / Shift+TabCommit and move right/left

Everything else

KeyAction
Ctrl+C / Ctrl+X / Ctrl+VCopy, cut, paste
DeleteClear selected cells
Ctrl+Z / Ctrl+YUndo / redo
Shift+F10Open context menu
Enter / Space on column headerToggle 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:

AttributePurpose
role="grid"Identifies the interactive grid container
aria-labelGives the grid an accessible name
aria-rowcountTotal rows (useful for server-side pagination where not all rows are in the DOM)
aria-rowindexRow position in the full dataset
aria-sortCurrent sort state on column headers
aria-selectedWhether a row is selected
aria-expandedOpen/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-label is 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

Found an accessibility issue? Open an issue and tag it accessibility with your browser, screen reader version, and reproduction steps.