Core ARIA & Keyboard Navigation for Data UIs

Permalink to "Core ARIA & Keyboard Navigation for Data UIs"

This reference covers the ARIA specification patterns and keyboard interaction contracts that underpin accessible data interfaces — grids, trees, dashboards, and filtered result sets. It targets frontend engineers and accessibility specialists who need to satisfy WCAG 2.2 success criteria while shipping production-quality components. The patterns here govern WCAG 2.1.1 (Keyboard), 2.4.3 (Focus Order), 2.4.7 (Focus Visible), 4.1.2 (Name, Role, Value), and 4.1.3 (Status Messages).


WCAG 2.2 & ARIA Compliance Anchor

Permalink to "WCAG 2.2 & ARIA Compliance Anchor"

The table below maps each WCAG success criterion to the specific data-UI failure mode it prevents. Use this as your triage starting point when an automated lint report flags a violation.

Criterion Level Relevance to Data UIs
1.3.1 Info & Relationships A role, aria-colcount, aria-rowcount, and header associations must expose the same structure conveyed visually.
2.1.1 Keyboard A Every grid cell, filter control, and sort trigger must be operable without a pointer device.
2.1.2 No Keyboard Trap A Focus traps in dialogs and filter drawers must provide a documented escape path (Escape key).
2.4.1 Bypass Blocks A Landmark regions let AT users skip repeated navigation; required before any data region.
2.4.3 Focus Order A Tab order must match the visual/logical sequence; programmatic focus shifts after mutations must land at the correct element.
2.4.7 Focus Visible AA Focus indicators on custom grid cells must meet minimum contrast and size requirements.
2.4.11 Focus Not Obscured AA Sticky headers or filter panels must not permanently cover the focused cell.
4.1.2 Name, Role, Value A Every custom widget must expose its role, accessible name, and current state to the accessibility tree.
4.1.3 Status Messages AA Sort completions, load states, and async errors must be communicated via aria-live without requiring focus movement.

Architectural Overview

Permalink to "Architectural Overview"

The four topic areas in this section each address a distinct layer of the accessibility stack. They are not independent: a role mapping error at layer 1 will silently corrupt what layer 3 announces.

ARIA Architecture Layers for Data UIs Four stacked layers: Layer 1 Semantic Structure (landmark HTML), Layer 2 Role Mapping (grid/treegrid/listbox), Layer 3 Focus Architecture (roving tabindex, focus trapping), Layer 4 Live Announcements (aria-live polite/assertive). Arrows show each layer depends on the one below it. LAYER 4 Live Announcements — aria-live polite / assertive, aria-atomic, role=alert LAYER 3 Focus Architecture — roving tabindex, focus trapping, focus restoration LAYER 2 Role Mapping — role=grid / treegrid / listbox, aria-sort, aria-expanded LAYER 1 Semantic Structure — landmark HTML, heading hierarchy, aria-label Each layer depends on the correctness of all layers below it

The sections that follow address each layer in implementation order — build upward from semantic structure.


Semantic Structure & Landmark Regions

Permalink to "Semantic Structure & Landmark Regions"

Data-heavy applications break traditional navigation flows when developers reach for div soups without landmark regions. Without <main>, <nav>, and region-labelled <section> elements, assistive technology users must parse the entire DOM linearly to find the data they need. This violates WCAG 2.4.1 (Bypass Blocks).

Problem framing. A dashboard with a sidebar, a filter bar, a data grid, and a status bar uses at least five distinct interaction zones. Screen reader users rely on landmark navigation (rotor in VoiceOver, F6/D in NVDA) to jump directly to the zone they need. Unlabelled landmarks are listed as generic “region” in the rotor, making them indistinguishable.

Core pattern.

<!-- WCAG 2.4.1: each landmark region gets a unique aria-label -->
<body>
  <header role="banner">
    <nav aria-label="Global navigation"><!-- skip link + site links --></nav>
  </header>

  <main id="app-content" aria-label="Data workspace">
    <!-- WCAG 1.3.6: identify input purpose in filter controls -->
    <section aria-label="Filter controls" id="filter-panel">
      <!-- filter form -->
    </section>

    <!-- aria-busy signals async load state (WCAG 4.1.3) -->
    <section aria-label="Query results" id="data-region" aria-busy="false">
      <!-- data grid rendered here -->
    </section>
  </main>

  <aside aria-label="Summary statistics"><!-- sparklines, KPIs --></aside>
</body>

Implementation checklist.

Keyboard & screen reader behaviour.

Action NVDA VoiceOver JAWS
D / rotor → Landmarks Lists all labelled regions Lists all labelled regions Insert+F3 shows form/landmark list
Focus enters <main> Announces “main landmark” Announces “main, Data workspace” Announces “main region”
Tab into aria-busy region Reads content as available Reads “busy” if region is loading Reads “busy” indicator

WCAG alignment. This section satisfies 2.4.1 (Bypass Blocks — A), 1.3.1 (Info & Relationships — A), and contributes to 2.4.3 (Focus Order — A) by setting the structural context that tab order traverses.


ARIA Role Mapping for Complex Data Structures

Permalink to "ARIA Role Mapping for Complex Data Structures"

Correct role assignment dictates how assistive technology interprets hierarchical and tabular relationships. Misapplied roles cause silent failures: the accessibility tree node exists, but the announced semantics mislead the user.

Problem framing. A virtualized grid built with <div> elements carries no implicit table semantics. Without explicit role="grid" and its required owned elements (rowgroup, row, gridcell), screen readers announce a flat sequence of text strings with no column or row context. Users cannot ask “what column am I in?” because the accessibility tree contains no column information.

Core ARIA role taxonomy for data widgets.

Widget type Root role Required owned elements Key states
Flat editable grid grid rowgrouprowgridcell aria-sort, aria-selected, aria-readonly
Expandable row grid treegrid rowgrouprowgridcell, aria-expanded on row aria-level, aria-setsize, aria-posinset
Single-select list listbox option aria-selected, aria-disabled
Sortable column header columnheader (inside grid) aria-sort="ascending|descending|none|other"

Annotated code block.

<!-- role=grid: WCAG 4.1.2 — exposes name, role, value for the composite widget -->
<div
  role="grid"
  aria-colcount="4"      <!-- total columns; required when not all columns are in DOM -->
  aria-rowcount="250"    <!-- total rows including header; required for virtual grids -->
  aria-label="Q3 Sales Data"
  aria-multiselectable="true"
>
  <div role="rowgroup">  <!-- maps to <thead> semantics -->
    <div role="row">
      <!-- aria-sort: WCAG 4.1.2 — must update synchronously with visual sort indicator -->
      <div role="columnheader" aria-sort="ascending" aria-colindex="1">Region</div>
      <div role="columnheader" aria-sort="none" aria-colindex="2">Revenue</div>
      <div role="columnheader" aria-sort="none" aria-colindex="3">Growth</div>
      <div role="columnheader" aria-sort="none" aria-colindex="4">Status</div>
    </div>
  </div>

  <div role="rowgroup">  <!-- maps to <tbody> semantics -->
    <div role="row" aria-rowindex="2" aria-selected="false">
      <!-- aria-colindex: required when virtual scroll renders a subset of columns -->
      <div role="gridcell" aria-colindex="1">APAC</div>
      <div role="gridcell" aria-colindex="2">$4.2M</div>
      <div role="gridcell" aria-colindex="3">+12%</div>
      <!-- aria-readonly: WCAG 4.1.2 — distinguishes read-only from editable cells -->
      <div role="gridcell" aria-colindex="4" aria-readonly="true">Active</div>
    </div>
  </div>
</div>

Implementation checklist.

WCAG alignment. Role mapping is the primary implementation surface for WCAG 4.1.2 (Name, Role, Value — A) and 1.3.1 (Info & Relationships — A).


Keyboard Focus Architecture

Permalink to "Keyboard Focus Architecture"

Composite data widgets must implement a single active tab stop. The roving tabindex pattern described in the ARIA Authoring Practices Guide satisfies WCAG 2.1.1 (Keyboard) by keeping tabindex="0" on exactly one cell at a time while all other cells carry tabindex="-1". Arrow keys navigate internally; Tab and Shift+Tab move focus into and out of the widget entirely.

Full implementation details and framework-specific gotchas are covered in Focus Management in Single Page Apps, including implementing roving tabindex for custom data grids.

Problem framing. Without roving tabindex, a 250-row, 6-column grid forces keyboard users to press Tab 1,500 times to reach content after the grid. This is a WCAG 2.1.1 failure. Tab flooding also breaks screen reader virtual-browse mode: NVDA and JAWS users who navigate by virtual cursor instead of Tab will encounter the grid as a flat sequence of cells with no column framing.

Core pattern.

// WCAG 2.1.1: arrow-key navigation keeps one tabindex=0 active at a time
function handleGridKeydown(event, cells, currentIndex, colCount) {
  const { key } = event;
  const rowCount = cells.length / colCount;
  const row = Math.floor(currentIndex / colCount);
  const col = currentIndex % colCount;
  let nextIndex = null;

  switch (key) {
    case 'ArrowDown':
      // Prevent page scroll; move focus one row down
      event.preventDefault();
      nextIndex = row < rowCount - 1 ? currentIndex + colCount : currentIndex;
      break;
    case 'ArrowUp':
      event.preventDefault();
      nextIndex = row > 0 ? currentIndex - colCount : currentIndex;
      break;
    case 'ArrowRight':
      event.preventDefault();
      nextIndex = col < colCount - 1 ? currentIndex + 1 : currentIndex;
      break;
    case 'ArrowLeft':
      event.preventDefault();
      nextIndex = col > 0 ? currentIndex - 1 : currentIndex;
      break;
    case 'Home':
      event.preventDefault();
      // WCAG 2.1.1: Home moves to first cell in the current row
      nextIndex = row * colCount;
      break;
    case 'End':
      event.preventDefault();
      // WCAG 2.1.1: End moves to last cell in the current row
      nextIndex = row * colCount + (colCount - 1);
      break;
    case 'Home': // Ctrl+Home: top-left cell
      if (event.ctrlKey) { event.preventDefault(); nextIndex = 0; }
      break;
    case 'End': // Ctrl+End: bottom-right cell
      if (event.ctrlKey) { event.preventDefault(); nextIndex = cells.length - 1; }
      break;
  }

  if (nextIndex !== null && nextIndex !== currentIndex) {
    // Shift the single tabindex=0 to the new active cell
    cells[currentIndex].setAttribute('tabindex', '-1');
    cells[nextIndex].setAttribute('tabindex', '0');
    cells[nextIndex].focus();
  }
}

Keyboard interaction contract.

Key Action Expected AT announcement Failure indicator
Tab Enter grid; focus lands on previously active cell “Region [name], row [n], column [n]” Focus jumps to first cell every time (tabindex not persisted)
Arrow keys Move one cell in that direction Cell content + row/col position No announcement (role missing or tabindex not updated)
Home First cell in current row Cell content + “column 1” Focus wraps to previous row
End Last cell in current row Cell content + column name Focus wraps to next row
Ctrl+Home Cell at row 1, column 1 Header cell content No movement (modifier not handled)
Ctrl+End Last data cell Cell content No movement
Enter / F2 Activate edit mode on editable cell “Editing [cell content]” Cell silently enters edit mode without announcement
Escape Exit edit mode, return to navigation “Stopped editing” Focus lost after Escape
Tab (inside widget) Exit grid entirely to next focusable element Next element announced Tab navigates between cells (roving tabindex not implemented)

When a modal overlay, filter drawer, or date picker opens from within the grid, a focus trap must constrain keyboard focus to that container. The Keyboard Focus Trapping & Navigation pattern handles this, including the critical case of restoring focus after closing complex modals — returning focus to the exact grid cell that triggered the overlay.

Screen reader behaviour.

AT + browser Cell navigation announcement Edit mode
NVDA + Firefox “Row [n], column [n], [cell content], [role]” “Editing” + cell content
JAWS + Chrome “[cell content], row [n] of [total], column [n] of [total]” “Edit, [content]”
VoiceOver + Safari “[content], row [n], column [n] of [total]” “Editing text field”
TalkBack + Chrome “[content], row [n], column [n]” Varies by Android version

WCAG alignment. This section directly implements WCAG 2.1.1 (Keyboard — A), 2.4.3 (Focus Order — A), 2.4.7 (Focus Visible — AA), and 2.4.11 (Focus Not Obscured — AA).


ARIA Live Regions for Dynamic Data

Permalink to "ARIA Live Regions for Dynamic Data"

Asynchronous updates — pagination, filter results, background sort, auto-refresh — are invisible to screen readers unless explicitly announced. Visual loading spinners, colour changes, and count badge updates are inaccessible to users who cannot see the page. WCAG 4.1.3 (Status Messages) requires that status information is programmatically determinable without focus movement.

Detailed implementation guidance and the polite-vs-assertive decision tree are in ARIA Live Regions for Dynamic Data, including choosing between polite and assertive aria-live regions.

Problem framing. A filtered grid that updates to show 14 matching rows gives no feedback to a screen reader user after they activate the filter. Their cursor remains on the filter button. Nothing announces that the table below changed. They must navigate back into the grid and count rows manually to understand what happened.

Core pattern.

<!-- WCAG 4.1.3: status messages must not require focus to be perceived -->

<!-- Polite region: queues announcement after current speech completes -->
<!-- Use for: sort complete, pagination, filter results, row count change -->
<div
  id="status-announcer"
  role="status"              <!-- implicit aria-live=polite, aria-atomic=true -->
  aria-live="polite"
  aria-atomic="true"         <!-- announce the full region text, not just the diff -->
  class="sr-only"
>
  <!-- Injected by JavaScript; cleared after 5 seconds to allow re-announcement -->
</div>

<!-- Assertive region: interrupts current speech immediately -->
<!-- Use for: session timeout, critical validation failure, authentication error -->
<div
  id="alert-announcer"
  role="alert"               <!-- implicit aria-live=assertive, aria-atomic=true -->
  aria-live="assertive"
  aria-atomic="true"
  class="sr-only"
>
</div>
// Inject status message — WCAG 4.1.3: communicated without focus change
function announceStatus(message, isUrgent = false) {
  const regionId = isUrgent ? 'alert-announcer' : 'status-announcer';
  const region = document.getElementById(regionId);

  // Clear first so identical consecutive messages still trigger re-announcement
  region.textContent = '';

  // Defer insertion to next frame so the DOM mutation is observed
  requestAnimationFrame(() => {
    region.textContent = message; // e.g. "Filter applied. 14 rows shown."
  });
}

// During async fetch: aria-busy blocks AT from reading incomplete content
function setLoadingState(dataRegion, isLoading) {
  dataRegion.setAttribute('aria-busy', isLoading ? 'true' : 'false');
  if (isLoading) {
    announceStatus('Loading results…');
  }
}

Announcement guidelines.

  • Keep messages under 15 words; AT truncates long live-region strings unpredictably.
  • Avoid consecutive identical messages — many screen readers suppress re-announcement of unchanged text. Clear the region text first (as above), then re-inject.
  • Include concrete counts: “23 rows sorted by Revenue, descending” not “Sort applied”.
  • Set aria-busy="true" on the data container during fetch so AT does not read partial content mid-load.

Behaviour by AT.

Update type NVDA VoiceOver JAWS
aria-live="polite" injection Queued after current utterance Queued; may delay 1–2s Queued after current speech
aria-live="assertive" injection Immediate interruption Immediate Immediate interruption
aria-busy="true" on region “Busy” announced on entry Does not read busy label “Busy” on entry
role="alert" Interrupts immediately Interrupts after brief pause Interrupts immediately

WCAG alignment. This pattern satisfies WCAG 4.1.3 (Status Messages — AA) and supports 2.4.3 (Focus Order — A) by eliminating the need to move focus to learn about state changes.


Screen Reader Announcement Strategies

Permalink to "Screen Reader Announcement Strategies"

The patterns in this section govern how screen readers narrate complex data state changes — row selection, column sort, filter activation, and multi-row operations. Correctly structured announcements allow AT users to maintain context without visually scanning the page. The Screen Reader Announcement Strategies topic covers the full range of announcement composition, including VoiceOver strategies for announcing table updates.

Core pattern — selection state. ARIA selection state (aria-selected="true|false") is read automatically by AT when focus moves to the row or cell. Trigger a supplementary live-region announcement only when a non-focus-changing selection action occurs (e.g. Space selecting a checkbox column cell while focus stays on the row).

<!-- aria-selected: WCAG 4.1.2 — exposes selection state without focus change -->
<div
  role="row"
  aria-rowindex="3"
  aria-selected="true"       <!-- read on focus entry AND on state change if announced -->
>
  <div role="gridcell" aria-colindex="1">
    <!-- Checkbox inside cell: selection mechanism -->
    <input
      type="checkbox"
      checked
      aria-label="Select row 3: APAC region"
    />
  </div>
  <div role="gridcell" aria-colindex="2">APAC</div>
</div>

Sort announcement pattern. When a column header sort is activated, update aria-sort synchronously and inject a status announcement. Do not rely on AT to automatically detect aria-sort changes — only focus movement to the header re-reads the attribute.

function sortColumn(headerCell, direction) {
  // 1. Update all headers to aria-sort=none first
  document.querySelectorAll('[role="columnheader"]').forEach(h => {
    h.setAttribute('aria-sort', 'none');
  });

  // 2. Set the active column's sort direction
  headerCell.setAttribute('aria-sort', direction); // WCAG 4.1.2

  // 3. Announce the result — WCAG 4.1.3
  const colName = headerCell.textContent.trim();
  announceStatus(`Table sorted by ${colName}, ${direction}.`);
}

Implementation checklist.


Cross-Cutting Concerns

Permalink to "Cross-Cutting Concerns"

These failure modes span all four layers and are the source of the most persistent accessibility bugs in production data UIs.

Focus Loss After DOM Mutations

Permalink to "Focus Loss After DOM Mutations"

When a component removes or replaces DOM nodes — virtual scroll buffer swap, paginating rows, deleting a selected row — focus is dropped to <body>. NVDA announces “Document” and the user has completely lost their position. This violates WCAG 2.4.3 (Focus Order) because the programmatic focus sequence is no longer predictable.

Fix. Before any DOM mutation that will remove the focused element, capture a reference to the element and a logical successor. After the mutation, call .focus() on the successor.

function deleteRow(row) {
  const grid = row.closest('[role="grid"]');
  const nextRow = row.nextElementSibling || row.previousElementSibling;

  row.remove(); // DOM mutation — would drop focus to <body>

  // Restore focus to the next logical position (WCAG 2.4.3)
  const firstCell = nextRow?.querySelector('[role="gridcell"]');
  if (firstCell) {
    firstCell.setAttribute('tabindex', '0');
    firstCell.focus();
    announceStatus('Row deleted. Focus moved to next row.');
  } else {
    // Last row deleted — return focus to the grid container
    grid.setAttribute('tabindex', '0');
    grid.focus();
    announceStatus('All rows deleted.');
  }
}

Live Region Collision

Permalink to "Live Region Collision"

Multiple live regions firing simultaneously produce garbled or dropped announcements. NVDA and JAWS queue polite regions sequentially but may discard earlier messages when a new injection arrives before the previous one finishes reading.

Fix. Use a single role="status" region for the page. Coordinate announcement timing with a debounce function that waits for the current announcement to finish before injecting the next.

let announceTimeout;
function debouncedAnnounce(message, delayMs = 300) {
  clearTimeout(announceTimeout);
  announceTimeout = setTimeout(() => announceStatus(message), delayMs);
}

Virtual DOM Reconciliation and tabindex

Permalink to "Virtual DOM Reconciliation and tabindex"

React, Vue, and Angular reconcilers can silently remove and re-add DOM nodes during re-renders. If tabindex="0" was on a node that gets replaced rather than updated in-place, the newly created replacement node carries the default tabindex (which is -1 for non-interactive elements). Focus is lost mid-navigation.

Fix. Use a stable key prop on every row and cell so the reconciler patches the existing node rather than replacing it. Manage tabindex via a ref or useEffect/onUpdated hook rather than embedding it in render state.

aria-sort Race Condition

Permalink to "aria-sort Race Condition"

Visual sort indicators and aria-sort attribute updates that are not committed atomically cause a window where AT reads one sort direction while the visual shows another. This is a WCAG 4.1.2 failure.

Fix. Update aria-sort in the same synchronous operation as the DOM re-render trigger. Do not set aria-sort in a setTimeout callback.

Focus Trap Leak on Escape

Permalink to "Focus Trap Leak on Escape"

A focus trap in a date picker or filter drawer that does not intercept Escape (or intercepts it but fails to restore focus) leaves keyboard users stranded outside the trap with no path back to the grid. See Keyboard Focus Trapping & Navigation for the canonical trap implementation and the Escape-restore pattern.


Testing & Validation Protocol

Permalink to "Testing & Validation Protocol"
  1. Automated lint. Run axe-core (or equivalent) against every rendered page state: initial load, after sort, after filter, after row delete, with dialog open. Automated checks catch ~35% of WCAG A/AA failures; they are necessary but not sufficient.

  2. Keyboard-only walkthrough. Unplug or disable the pointer device. Navigate the entire data workflow using only Tab, Shift+Tab, arrow keys, Enter, Space, and Escape. Verify every interactive element is reachable, operable, and produces visible focus indicators (WCAG 2.4.7).

  3. AT matrix — manual testing.

    AT + browser Grid navigation Live regions Focus traps Sort announce
    NVDA 2024 + Firefox Pass / Fail Pass / Fail Pass / Fail Pass / Fail
    JAWS 2024 + Chrome Pass / Fail Pass / Fail Pass / Fail Pass / Fail
    VoiceOver macOS + Safari Pass / Fail Pass / Fail Pass / Fail Pass / Fail
    VoiceOver iOS + Safari Pass / Fail Pass / Fail Pass / Fail Pass / Fail
    TalkBack + Chrome Android Pass / Fail Pass / Fail Pass / Fail Pass / Fail
  4. Focus order audit. Tab through the page logging every focus target. Compare against the expected logical sequence. Any gap (focus drops to <body>, skip link bypasses content, modal does not trap) is a regression item.

  5. Regression tracking. Maintain a living accessibility matrix in your project’s issue tracker. Tag failures with the WCAG criterion, the AT + browser combination, and the component responsible. Re-run the AT matrix on every release that touches grid, dialog, or live-region code.

  6. Focus indicator contrast. Use a colour contrast tool to verify that focus rings on custom cells meet WCAG 2.4.11 (component boundary must have at least 3:1 contrast against adjacent colours) in both light and dark themes.


Design System Integration

Permalink to "Design System Integration"

Data grid accessibility tokens must be documented alongside their WCAG rationale so component library consumers do not accidentally override them.

Required token categories.

Token Purpose WCAG criterion
--focus-ring-color Focus indicator colour 2.4.7, 2.4.11
--focus-ring-width Minimum 2px outline width 2.4.7
--focus-ring-offset Space between cell boundary and ring 2.4.11
--cell-selected-bg Selected row background 1.4.1 (not colour-only)
--cell-selected-border Selected row border (non-colour indicator) 1.4.1
--sr-only-clip Off-screen positioning for .sr-only utility

Component variant mapping.

Component variant ARIA roles required WCAG criteria
Read-only data table <table>, <th scope>, <caption> 1.3.1
Sortable data grid role=grid, aria-sort on columnheader 1.3.1, 4.1.2
Editable grid As above + aria-readonly, aria-required on cells 4.1.2
Tree grid role=treegrid, aria-expanded, aria-level 4.1.2
Virtualized grid As above + aria-rowcount, aria-rowindex 1.3.1, 4.1.2

Design tokens that override outline or box-shadow on [role="gridcell"]:focus must be audited. Browser-default focus outlines are frequently stripped by CSS resets; the token system must restore them to at minimum a 2px solid ring with 3:1 contrast against the cell background.


FAQ

Permalink to "FAQ"
When should I use role="grid" instead of a native <table>?

Use role="grid" when the widget requires arrow-key navigation between interactive cells (inline editing, cell-range selection) and the layout cannot be built with a native <table> element. Native <table> with <th scope> is always preferred for read-only tabular data: it carries implicit ARIA semantics for free and requires no JavaScript to function. role="grid" is the right tool for data widgets that behave more like a spreadsheet than a report.

What is the difference between aria-live="polite" and aria-live="assertive"?

aria-live="polite" queues the announcement until the screen reader finishes its current utterance. Use it for non-critical updates: sort completions, pagination changes, filter result counts. aria-live="assertive" interrupts whatever the screen reader is currently saying. Reserve it for conditions that require immediate user action: session timeouts, authentication errors, critical form validation failures. Overusing assertive trains users to ignore interruptions.

Does roving tabindex work with virtual DOM frameworks like React?

Yes, with one critical caveat: reconciliation timing. If tabindex="0" is set via component state, a re-render that replaces (rather than patches) the DOM node will silently reset it. Assign tabindex via a ref and useLayoutEffect to ensure the attribute is applied to the live DOM node before focus() is called. Use stable key props on rows and cells so the reconciler patches rather than replaces.


Permalink to "Related"

← Back to Home