AG Grid React Keyboard and Screen Reader Setup

Permalink to "AG Grid React Keyboard and Screen Reader Setup"

AG Grid is a powerful enterprise grid with documented accessibility support: ARIA roles and attributes, a full keyboard navigation model, focus management for headers, cells and floating filters, and options that trade performance for screen reader friendliness. Out of the box it is usable; configured carefully it is good. Most accessibility problems in AG Grid implementations come from custom renderers, virtualization defaults and missing announcements rather than from the grid itself.

This guide belongs to React data grid libraries.

Spec reference

Permalink to "Spec reference"
  • ARIA grid pattern — AG Grid renders role="grid" (or treegrid for tree data), rows and cells with indices, aria-sort, aria-expanded for groups, and aria-selected.
  • ensureDomOrder — renders rows and columns in DOM order matching visual order, which helps screen readers that read the DOM sequentially. It can cost performance and disables some animations.
  • suppressColumnVirtualisation / rowBuffer — column virtualization removes off-screen columns from the DOM; disabling it lets screen readers access all columns of rendered rows.
  • SC 4.1.2 (A), SC 2.1.1 (A), SC 4.1.3 (AA) — names, keyboard and status messages.
Default AG Grid versus accessibility-tuned AG Grid Comparison: defaults favour performance with column virtualization, no grid name and no announcements; tuned settings name the grid, keep DOM order, render all columns and announce changes. Default AG Grid versus accessibility-tuned AG Grid✗ DefaultsNo accessible nameColumn virtualization onDOM order may differ from visualCustom renderers steal focusSilent sort and filter✓ Accessibility-tunedaria-label via gridOptions or wrappersuppressColumnVirtualisation for moderatecolumn countsensureDomOrder: truesuppressKeyboardEvent and focus forwardingStatus region announcements

When to use — and when not to

Permalink to "When to use — and when not to"

Use AG Grid for complex enterprise grids — grouping, pivoting, editing, large datasets. Use ensureDomOrder and disabled column virtualization when column counts are moderate (tens, not hundreds) and screen reader access matters, which it always does.

For very large column counts, keep column virtualization and ensure aria-colcount and aria-colindex are correct (AG Grid sets them) so position is announced.

The misapplication: a custom cell renderer with buttons that are individually tab-focusable, turning one tab stop into hundreds and breaking the grid’s arrow-key model.

Tuning decisions for AG Grid Flow: name the grid; choose DOM order and column virtualization based on column count; check renderers for focus; add announcements; test with readers. Tuning decisions for AG GridName the gridaria-label or labelledbywrapper or gridOptionsColumns under ~50?suppressColumnVirtualisationall columns readableReading order matters?ensureDomOrder: trueDOM matches visualCustom renderers?no own tab stops; Enter to interactsuppressKeyboardEventAnnouncementssort, filter, page, editsstatus region

Annotated code example

Permalink to "Annotated code example"
import { AgGridReact } from 'ag-grid-react';

function Invoices({ rowData }) {
  const [msg, setMsg] = useState('');
  const gridOptions = useMemo(() => ({
    ensureDomOrder: true,                          // DOM order = visual order for readers
    suppressColumnVirtualisation: true,            // all columns in DOM for rendered rows
    rowBuffer: 20,                                 // a few more rows around the viewport
    onSortChanged: (e) => {
      const s = e.api.getColumnState().find(c => c.sort);
      setMsg(s ? `Sorted by ${headerName(s.colId)}, ${s.sort === 'asc' ? 'ascending' : 'descending'}` : 'Sort cleared');
    },
    onFilterChanged: (e) => setMsg(`${e.api.getDisplayedRowCount()} invoices shown`),
  }), []);

  return (<>
    <div className="ag-theme-quartz" style={{ height: 600 }}>
      <AgGridReact rowData={rowData} columnDefs={cols} gridOptions={gridOptions}
                   aria-label="Invoices" />                {/* verify your version forwards it; else wrap with role=region */}
    </div>
    <p role="status" className="visually-hidden">{msg}</p>   {/* SC 4.1.3 */}
  </>);
}

// Custom renderer with an action button: let the grid own focus
const ActionsRenderer = (p) => (
  <button type="button" tabIndex={-1} aria-label={`Actions for ${p.data.id}`}
          onClick={() => openMenu(p.data)}>⋯</button>
);
const cols = [{ field: 'actions', cellRenderer: ActionsRenderer,
  suppressKeyboardEvent: ({ event }) => event.key === 'Enter' && (openMenu(event), true) }];

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Key AG Grid behaviour Expected announcement Notes
Tab Into grid header or first cell Header or cell with name and position Tab also moves through header floating filters
Arrows Cell navigation Header on column change; value Provided
Enter on header Sort Header with sort state; your status Status needs your region
Enter on cell Edit or activate renderer Editor or your action Use suppressKeyboardEvent for custom actions
Ctrl+Enter / Escape Stop editing Cell value Provided
Shift+Space Select row (if enabled) Selected state Provided
Virtualization settings and screen reader access Matrix of settings — default virtualization, suppressColumnVirtualisation, ensureDomOrder, both — against column access, reading order, performance cost and recommendation. Virtualization settings and screen reader accessColumn accessReading orderPerformance costRecommendationDefaultsVisible columnsonlyMay differLowestHuge gridsNo column virtualizationAll columnsMay differModerateMost gridsensureDomOrderVisible columnsMatches visualModerateMost gridsBothAll columnsMatches visualHigherModerate grids
For moderate grids, use both; for huge grids, rely on indices.

Integration context

Permalink to "Integration context"

AG Grid’s inline editing is covered in AG Grid inline editing accessibility. Large-grid trade-offs are discussed in DOM size limits and accessible performance tradeoffs. Row action renderers should follow row action menus with the menu button pattern.

Gotchas

Permalink to "Gotchas"

Floating filters. Each floating filter adds tab stops in the header. Ensure each input has a label (“Filter Amount”) and consider whether all columns need them.

Themes. AG Grid themes set focus and selection colours; custom themes must keep focus outlines at 3:1 — test dark themes especially.

Grid name. Whether aria-label on the React component reaches the grid element depends on version; check the rendered DOM. If not, wrap the grid in a labelled region and set the label via the API or DOM after ready.

Common mistakes

Permalink to "Common mistakes"
  • Renderer buttons with their own tab stops.
  • Column virtualization hiding columns from screen readers on moderate grids.
  • No announcements for sort and filter.
  • Unlabelled floating filters.

Version and upgrade notes

Permalink to "Version and upgrade notes"

AG Grid’s accessibility features and option names evolve across major versions (for example, row selection options were reorganised in recent releases). Pin your version, read the accessibility section of the changelog on upgrades, and run keyboard and screen reader smoke tests on the upgrade branch before merging.

Worked example

Permalink to "Worked example"

A trading desk grid with 30 columns and custom renderers had 300 tab stops per screen (a button in every row’s actions cell) and hid columns beyond the viewport from screen readers. Setting renderer buttons to tabIndex={-1} with Enter handled through suppressKeyboardEvent, disabling column virtualization and enabling ensureDomOrder restored a single tab stop and made every column readable. A status region for sorting and filtering completed the fix. Performance remained acceptable at 30 columns and 5,000 rows.

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Provide an InvoiceGrid-style wrapper preset with accessibility options, renderer helpers that respect grid focus, and the shared announcer. Document column-count thresholds for virtualization settings.

FAQ

Permalink to "FAQ"
Does AG Grid support screen readers?

Yes, with ARIA roles, keyboard navigation and options for DOM order. Test with your target readers and configure as above.

Should I always disable column virtualization?

For moderate column counts, yes. For hundreds of columns, keep it and rely on aria-colindex.

How do I make custom renderers accessible?

Name their controls, keep them out of the tab order, and handle activation through the grid’s key events.

Is AG Grid's treegrid accessible?

It exposes treegrid semantics for tree data and row groups; verify expand and collapse announcements in your readers.

Does AG Grid announce sorting?

It sets aria-sort. A status message describing the new order is recommended and is yours to add.

What about the Excel export?

Check exported headers and values; use processCellCallback for raw values.

How do I name the grid if the prop is not forwarded?

Wrap the grid in an element with role=“region” and aria-labelledby pointing at the heading, and set aria-label on the grid root after onGridReady if needed. Check the rendered DOM either way.

Permalink to "Related"

← Back to React Data Grid Libraries