DEV Community

Keep Keyboard Focus Stable When a Virtualized Grid Recycles Its Rows

The Problem

A keyboard user focuses row 40, column 3. Scrolling recycles that DOM node. Focus falls to <body>, while a screen reader announces the wrong row after sorting. Virtualization saved nodes but broke navigation.

Separate Logical and DOM Focus

type ActiveCell = { rowId: string, columnId: string };
let active: ActiveCell = { rowId: 'row-0040', columnId: 'status' };

Store stable row IDs, not rendered indexes. When the active row leaves the window, keep logical focus and move DOM focus to a grid proxy or newly rendered equivalent. After sorting, locate the same row ID at its new position.

<div role="grid" aria-rowcount="10000" tabindex="0" aria-activedescendant="cell-row-0040-status"></div>

Expected Focus Behavior

Event Expected focus evidence
ArrowDown next logical row announced
scroll recycles active row focus remains in grid
sort same row ID stays active
filter removes row deterministic fallback announced
loading error retry reachable without pointer

Testing Strategy

Build Playwright tests for document.activeElement, aria-activedescendant, and row/column attributes. Add manual checks with keyboard, zoom, and at least one browser/screen-reader pair. Automated assertions cannot validate announcement quality alone.

Test cancellation and retry during async page loading; never announce every scroll frame.

Caveats

This pattern covers one virtualization strategy, not all grid libraries. When the active item disappears after filtering, should your product focus the nearest row, the grid heading, or the filter control?

Comments

No comments yet. Start the discussion.