React

Verified against Claude Code · 2026-07-26

Virtualize a long list or table without losing keyboard and screen-reader support

Windows a large dataset with react-window or TanStack Virtual, choosing between fixed and measured row heights correctly and preserving roving-tabindex keyboard navigation and ARIA row semantics that off-screen virtualization would otherwise silently break.

Claude CodeCursorGitHub Copilot ChatChatGPT5 fillable variables

The prompt

Ready to copy — highlighted parts are example details you can swap.

You are adding virtualization to a React list or table that is currently rendering every row into the DOM at once. You pick a windowing strategy that fits the actual data shape, and you explicitly preserve keyboard navigation and screen-reader semantics that virtualization can silently break if the rows outside the viewport simply don't exist in the DOM.

LIST OR TABLE
A transaction history table for an accounting tool, showing every line item for an account.

ITEM COUNT AND SHAPE
Typically 5,000 to 40,000 rows per account; every row has the same three-line layout.

ROW HEIGHT BEHAVIOR
Fixed height, 56px per row, no wrapping text.

INTERACTION REQUIREMENTS
Must support arrow-key row navigation and Enter to open a row, and must announce total row count to screen readers.

CURRENT IMPLEMENTATION
A plain .map() over the full array rendering a <TransactionRow> for each item inside a scrollable div — no virtualization at all today.

VIRTUALIZATION RULES
Choose a fixed-size list implementation only if every row is genuinely the same height; choose a variable-size or dynamically-measured implementation if row height depends on content, and be explicit that measured rows require an actual measurement pass — usually a ResizeObserver-backed hook — not a guessed average height, since a guessed height causes visible jump-scrolling the moment a real measurement replaces the estimate. Set overscan to render a small number of extra rows above and below the visible window, large enough that fast scrolling or arrow-key navigation doesn't visibly flash empty space, but not so large that it defeats the purpose of virtualizing in the first place. If the list needs to be keyboard-navigable, implement roving tabindex on the visible, rendered rows only, and make sure focus is explicitly re-established on the correct row after a scroll-triggered re-render swaps which DOM nodes exist — a naive virtualization will drop focus entirely the moment the focused row scrolls out of the rendered window and gets unmounted. Expose the correct ARIA semantics for the container's actual role — aria-rowcount and aria-rowindex on a table-like structure, or aria-setsize and aria-posinset on a listbox-like structure — set to the full logical count and index, not just the count of rows currently mounted in the DOM, so a screen-reader user is told "row 340 of 12,000" and not "row 4 of 9." If the table has a sticky header row, keep it outside the virtualized scroll container so it never gets unmounted along with off-screen body rows, and account for its height separately when calculating the visible window instead of treating it as just another row the virtualizer manages.

OUTPUT FORMAT
1. Which virtualization library and row-height mode you chose, and the one-sentence reason tied to the stated row height behavior.
2. The implementation, as real code, including the overscan value chosen and why.
3. The keyboard-navigation and ARIA handling, called out as its own section, not folded silently into the general implementation.
4. Anything about the current implementation that will need to change beyond just wrapping it in the virtualizer — a CSS assumption that every row is in normal flow, for instance.

Customize

Optional — swap in your own details for the highlighted parts above.

Why this works

The fixed-versus-measured row-height decision is the actual fork in the road for this entire class of problem, not a minor implementation detail: a fixed-size virtualizer can compute exact scroll positions instantly because every row's height is known up front, while a variable-size list has to measure real rendered content, which means the very first render of any row is necessarily a guess that then gets corrected — building this without acknowledging that guess-then-correct cycle is why so many hand-rolled virtualized lists visibly jump or overshoot when the user scrolls fast, and naming it explicitly forces the model to either commit to a fixed height, honestly, or implement the ResizeObserver-based measurement pass a variable list actually requires. The keyboard-focus rule targets a failure mode that is invisible in a quick visual test but breaks the feature entirely for a real keyboard user: the whole mechanism of virtualization is unmounting DOM nodes that scroll out of view, and if the currently-focused row is one of them, the browser's focus silently falls back to the document body with no visible indication anything went wrong, which a sighted mouse user testing the feature will never notice and a keyboard-only user will hit on literally every scroll. Setting ARIA row count and index to the full logical dataset size rather than the currently-mounted DOM count matters because a screen reader has no other way to know a virtualized list even has 12,000 rows — from its perspective, without the correct attributes, the list simply looks like it has nine items, which is a materially false statement about the data, not just a minor omission. The sticky-header caveat matters because a header naively rendered inside the same virtualized list as the body rows will itself scroll out of the rendered window under fast scrolling, and a missing header is an especially visible failure since it's the one part of the table every user is looking at, unlike a missing off-screen row nobody was looking at anyway.

What you get back

Chosen: react-window's FixedSizeList — every row is a uniform 56px per the stated row height behavior, so a fixed-size virtualizer avoids the measurement pass entirely and gives exact scroll-position math. Overscan set to 6 rows above and below the viewport, enough to avoid visible flashing on a fast arrow-key repeat without rendering hundreds of unneeded rows. <FixedSizeList height={600} itemCount={transactions.length} itemSize={56} overscanCount={6} outerElementType={TableBody} itemData={transactions}> {Row} </FixedSizeList> Keyboard/ARIA: roving tabindex tracked in a ref (not per-row state, since rows unmount), re-applied via a scroll-to-item + focus() call after any arrow-key move that would scroll the target row into view. Container gets role="grid" with aria-rowcount={transactions.length}; each rendered Row gets aria-rowindex set to its real index in the full array, not its position among currently-mounted rows.

Verified against

Claude Code Sonnet 4.6 · 2026-07-26

Cursor Cursor 2.1 · 2026-08-03

Changelog

  • 2026-07-26 Initial publish, verified against Claude Code (Sonnet 4.6) and Cursor 2.1 using react-window 2 and TanStack Virtual 3.

Need this built into your business?

If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.

EXPLORE CUSTOM SOFTWARE
All React prompts

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY