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, with afterGuiAttached() (or a useEffect in React) as the place to move focus into the editor.
  • stopEditingWhenCellsLoseFocus — commits on blur rather than leaving an orphaned editor.
  • onCellValueChanged and onCellEditingStopped — 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.

What AG Grid gives you, and what it cannot Comparison of accessibility features AG Grid provides by default against the pieces an application must add around inline editing. What AG Grid gives you, and what it cannot✓ Provided by AG Gridrole="grid", row and column indices,aria-rowcountArrow, Home, End, Page key navigationEnter and F2 to edit; Escape to cancelaria-sort on sortable headersFocus restored to the cell after editing✗ Yours to addCustom editors that are keyboard operableEditor names that include the rowValidation messages bound to the editorSave and failure announcementsKeyboard help for grid shortcuts
The right-hand column is exactly the code your team writes — which is why it is where the bugs are.

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
One edit through AG Grid, with the added pieces Flow of one inline edit through AG Grid, marking which stages are built in and which the application adds: start editing, custom editor, validation, commit, save announcement. One edit through AG Grid, with the added piecesStart editbuilt in: Enter orF2Custom editoryours: nativeinput, namedValidateyours: aria-invalidand messageCommitbuilt in: focus backto cellSave feedbackyours: statusmessage
Stages two, three and five are application code — the places to test hardest.

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.

AG Grid options with accessibility impact Matrix of AG Grid options, what each changes for assistive technology users, and its cost. AG Grid options with accessibility impactOptionEffect for AT usersCostensureDomOrderBrowse order matches screenSlower row renderingsuppressColumnVirtualisationAll columns readableHeavy with many columnsstopEditingWhenCellsLoseFocusNo orphaned editorsNonesuppressKeyboardEvent(broad)Can trap focusBreaks Tab and Escape
Two of these are near-free wins; the other two are trade-offs to make deliberately.

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.

Permalink to "Related"

← Back to Inline Editing & Form Controls