Making AG Grid Inline Editing Accessible
Permalink to "Making AG Grid Inline Editing Accessible"AG Grid implements the ARIA grid pattern: role="grid" or treegrid, row and column indices, aria-rowcount for virtualised rows, arrow-key navigation, and Enter/F2 to start editing. Out of the box, the navigation half of the keyboard contract is in good shape. The editing half is where accessibility regressions come from, because it is the half teams customise — custom cell editors, validation, save indicators — and AG Grid cannot make those accessible for you.
This page lists what to leave alone, what to configure, and what to add. It applies the general contract from entering and exiting cell edit mode to AG Grid’s API, under inline editing & form controls.
Spec reference
Permalink to "Spec reference"The relevant AG Grid options and interfaces (v31 and later):
ensureDomOrder: true— keeps row and column DOM order equal to visual order, so browse-mode reading follows the screen. Costs some rendering performance.suppressColumnVirtualisation— renders all columns; useful when the column count is small, because virtualised columns are invisible to browse-mode reading.ICellEditorComp/ React cell editor components, withafterGuiAttached()(or auseEffectin React) as the place to move focus into the editor.stopEditingWhenCellsLoseFocus— commits on blur rather than leaving an orphaned editor.onCellValueChangedandonCellEditingStopped— the events to hang save feedback on.
On the WCAG side: SC 2.1.1 Keyboard and 2.1.2 No Keyboard Trap for editors, SC 4.1.2 Name, Role, Value for editor naming, SC 3.3.1 Error Identification for validation, and SC 4.1.3 Status Messages for save results.
When to customise — and when to leave defaults alone
Permalink to "When to customise — and when to leave defaults alone"Leave the navigation model alone. Overriding navigateToNextCell or tabToNextCell to create a clever custom order is the fastest way to break the grid for screen reader users, whose expectations come from the ARIA pattern AG Grid already follows. Customise only when a documented requirement demands it, and document the result in the grid’s help.
Customise editors freely, but build them on native elements. An AG Grid custom editor is just a component rendered into the cell; its accessibility is the accessibility of whatever you render.
The misapplication to name: disabling AG Grid’s keyboard handling (suppressKeyboardEvent returning true broadly) to stop it interfering with a custom editor. That also removes Escape and Tab handling, and trapped users follow. Suppress only the specific keys the editor needs, and only while it is open.
Annotated code example
Permalink to "Annotated code example"// React cell editor for a numeric Amount column
import { forwardRef, useEffect, useImperativeHandle, useId, useRef, useState } from 'react';
export const AmountEditor = forwardRef(function AmountEditor(props, ref) {
const [value, setValue] = useState(props.value);
const [error, setError] = useState('');
const input = useRef(null);
const errId = useId();
// Focus the input once AG Grid has attached the editor (SC 2.1.1)
useEffect(() => { input.current?.focus(); input.current?.select(); }, []);
useImperativeHandle(ref, () => ({
getValue: () => Number(value),
// Returning true cancels the commit; the editor stays open with the error
isCancelAfterEnd: () => Boolean(error),
}));
const onChange = (e) => {
setValue(e.target.value);
setError(Number.isFinite(Number(e.target.value)) ? '' : 'Enter a number, for example 1280.50');
};
return (
<div className="cell-editor">
<input
ref={input}
inputMode="decimal"
value={value}
onChange={onChange}
// SC 4.1.2: column + row, e.g. "Amount, SO-2202"
aria-label={`${props.colDef.headerName}, ${props.data.id}`}
// SC 3.3.1: the error is programmatically tied to the field
aria-invalid={error ? 'true' : undefined}
aria-describedby={error ? errId : undefined}
/>
{error && <span id={errId} className="cell-error">{error}</span>}
</div>
);
});
// Grid options: reading order, blur commit, save feedback
const gridOptions = {
ensureDomOrder: true, // browse-mode order = visual order
stopEditingWhenCellsLoseFocus: true, // no orphaned editors
columnDefs: [{ field: 'amount', headerName: 'Amount', editable: true, cellEditor: AmountEditor }],
async onCellValueChanged(e) {
try {
await save(e.data);
announce(`${e.colDef.headerName} for ${e.data.id} saved.`); // SC 4.1.3
} catch {
e.node.setDataValue(e.colDef.field, e.oldValue); // honest state
announce(`${e.colDef.headerName} for ${e.data.id} could not be saved.`, 'assertive');
}
},
};
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Key / event | Expected announcement | AT-specific deviations |
|---|---|---|
| Arrow to cell | “1,280.00, Amount, row 3 of 1,200” | JAWS reads row index from aria-rowindex; NVDA may omit it in browse mode |
Enter |
“Amount, SO-2202, edit text, 1,280.00” | VoiceOver reads the name after the value |
| Invalid input | “invalid entry, Enter a number…” on next focus or read | NVDA reads the description after a short pause |
Enter with error |
Editor stays open; error re-read | Without isCancelAfterEnd the bad value commits silently |
| Save succeeds | “Amount for SO-2202 saved.” | Polite; may queue behind the cell re-read |
Integration context
Permalink to "Integration context"AG Grid virtualises rows by default, so browse-mode reading only reaches rendered rows; the grid compensates with aria-rowcount and aria-rowindex. The general trade-offs are covered in accessible virtualized list patterns. If users need to read the whole data set linearly, offer an export or a paginated table view rather than disabling virtualisation on a 50,000-row grid.
The validation behaviour follows inline form validation inside editable table cells: the error stays with the field, focus stays in the field, and nothing is committed until it is valid.
Gotchas
Permalink to "Gotchas"Popup editors. cellEditorPopup: true renders the editor outside the cell. Focus handling still works, but the editor is no longer inside the gridcell in the accessibility tree, so its name must carry the column and row explicitly — the aria-label in the example matters more here.
Full-row editing. editType: 'fullRow' opens every editor in the row at once and uses Tab between them. Announce the row being edited, and make sure Escape cancels the whole row as documented.
Theme focus rings. Some AG Grid themes use a thin, low-contrast cell focus border. Check it against SC 2.4.11 as described in designing focus indicators for dense grids.
Testing checklist
Permalink to "Testing checklist"FAQ
Permalink to "FAQ"Why does AG Grid sometimes read the wrong row number?
Because rows are virtualised and the reader relies on aria-rowindex and aria-rowcount, which AG Grid sets. Problems appear when row DOM order differs from visual order; enabling ensureDomOrder fixes most of them.
Is AG Grid accessible out of the box?
Its navigation model is: it implements the ARIA grid pattern with row and column indices, arrow-key navigation and edit-mode keys. The accessibility of custom cell editors, validation messages and save feedback depends on your code, and those are where most audit findings come from.
How do I focus a custom AG Grid cell editor?
Move focus into the editor’s input once AG Grid has attached it — afterGuiAttached in a class component, or a mount effect in a React function component — and select its contents so typing replaces the value.
Should I set ensureDomOrder in AG Grid?
Yes when screen reader users will read the grid in browse mode, which is most grids. It keeps DOM order equal to visual order so linear reading matches the screen, at a modest rendering cost.
Related
Permalink to "Related"- Entering and exiting edit mode — the contract AG Grid implements
- Inline validation in editable cells — the error pattern to add
- Accessible virtualized list patterns — why AG Grid’s row virtualisation affects reading