Behaviors
Keyboard navigation, selection, editing, and clipboard behaviors.
W3C APG Compliance
We follow W3C ARIA Authoring Practices Guide — Grid Pattern for data grids.
| Aspect | W3C Spec | Our implementation |
|---|---|---|
| Grid is single tab stop | Required | Roving tabindex |
| Arrow navigation without wrap | Required for data grid | Default, wrap optional |
| Shift+Arrow extends selection | Recommended | Supported |
| Enter/F2 for edit mode | Recommended | Supported |
| Escape exits edit | Recommended | Supported |
| aria-selected for selection | Required if selection | Supported |
| aria-readonly for editability | Required if editing | Supported |
| aria-rowindex/colindex for virtual | Required if virtualized | Userland applies |
Note: W3C distinguishes "data grid" from "layout grid". We implement data grid (spreadsheet-like). Layout grid has different behaviors (wrap allowed).
Focus & Interaction Modes
Responsibility
Tracks which single element has keyboard focus and the current interaction mode. The mode determines how keyboard input is interpreted.
State
focus: {
target: CellRef | HeaderRef | null;
mode: "navigation" | "edit" | "interactive";
}
// Editing state (only set when mode === 'edit')
draft: unknown | null;
original: unknown | null;Focus Modes
| Mode | Description | Arrow Keys | Enter/F2 | Escape |
|---|---|---|---|---|
navigation | Default. Grid keyboard navigation active | Move between cells | Enter edit/interactive | Clear selection |
edit | Inline text editing in focused cell | Cursor in input | Commit + exit | Cancel + exit |
interactive | Complex interactive active (combobox, etc) | Interactive-specific | Interactive-specific | Exit to navigation |
Keyboard by Mode
Navigation Mode (default)
| Key | Action | Boundary behavior |
|---|---|---|
→ | Focus cell to right | Stop at last column |
← | Focus cell to left | Stop at first column |
↓ | Focus cell below | Stop at last row |
↑ | Focus cell above | Stop at first row |
Home | Focus first cell in row | — |
End | Focus last cell in row | — |
Ctrl+Home | Focus first cell in grid | — |
Ctrl+End | Focus last cell in grid | — |
Page Down | Future (Focus N rows below) | Not implemented in v1 |
Page Up | Future (Focus N rows above) | Not implemented in v1 |
Tab | Exit grid | Focus next element in document |
Escape | Clear selection | — |
Enter | Enter edit mode | If cell is editable |
Enter | Enter interactive mode | If cell is composite |
F2 | Enter edit mode | If cell is editable |
F2 | Enter interactive mode | If cell is composite |
| Typing | Enter edit mode + type char | If startOnType: true |
Edit Mode
| Key | Action | Notes |
|---|---|---|
| Arrow keys | Move cursor in input | Passed to input element |
Home | Cursor to start of input | Passed to input element |
End | Cursor to end of input | Passed to input element |
Ctrl+A | Select all text in input | NOT select all cells |
Ctrl+Z | Undo in input | Passed to input element |
Ctrl+Arrow | Word navigation in input | Passed to input element |
Backspace/Delete | Delete character | Passed to input element |
Enter | Commit edit, exit to navigation | Generates COMMIT_VALUE |
Tab | Commit edit, move to next cell | Moves focus right |
Shift+Tab | Commit edit, move to prev cell | Moves focus left |
Escape | Cancel edit, exit to navigation | Restores original value |
F2 | No-op | Already in edit mode |
Rationale: During edit, all keys except lifecycle keys (Enter, Tab, Escape) are passed to the input. This matches standard web form UX where arrow keys move the cursor, not navigate away.
Interactive Mode
| Key | Action | Notes |
|---|---|---|
| Arrow keys | Passed to interactive | E.g., navigate dropdown |
Tab | Cycle internal focusables | Exit on last element |
Shift+Tab | Cycle internal focusables (back) | Exit on first element |
Escape | Exit to navigation | May close popup first |
Enter | Interactive-specific | E.g., select dropdown option |
| Other keys | Passed to interactive | Interactive handles internally |
Note: Interactive mode keyboard handling is userland's responsibility. The library only manages mode transitions and passes events through.
Tab Behavior Matrix
| Current Mode | Cell Type | Tab Pressed | Behavior |
|---|---|---|---|
navigation | any | Tab | Exit grid, focus next element |
edit | — | Tab | Commit, exit, move to next cell |
interactive | multiple | Tab (not on last) | Cycle to next internal element |
interactive | multiple | Tab (on last) | Exit interactive mode, move to next cell |
interactive | single | Tab | Exit interactive mode, move to next cell |
Mode Transitions
┌─────────────────────────────────────────┐
│ │
▼ │
┌──────────────┐ Enter/F2/type ┌─────────────────┐
│ navigation │ ──────────────────► │ edit │
└──────────────┘ (editable cell) └─────────────────┘
│ │
│ Enter/F2 (composite) Enter/Tab │ Escape
▼ ▼
┌──────────────┐ ┌─────────────────┐
│ interactive │ ◄────────────────────│ navigation │
└──────────────┘ Escape/Tab out └─────────────────┘| From | To | Trigger | Preconditions |
|---|---|---|---|
navigation | edit | Enter, F2, or printable key | Cell is editable |
navigation | interactive | Enter, F2 | Cell is composite |
edit | navigation | Enter (commit), Escape (cancel) | — |
edit | navigation | Tab (commit + move) | Moves focus to next cell |
interactive | navigation | Escape, or Tab on last element | — |
Interactive Mode Details
For cells with internal interactives (combobox, checkbox group, date picker, multiple links).
| Condition | Behavior |
|---|---|
| Cell is composite | Enter/F2 enters interactive mode |
| In interactive mode, Tab | Cycles between internal elements |
| In interactive mode, Tab on last | Exits, returns to navigation mode |
| In interactive mode, Escape | Exits immediately |
| In interactive mode, Arrow keys | Passed to interactive (don't navigate grid) |
Interactive vs Edit: When a cell contains a combobox or date picker, use interactive mode, not edit mode. Edit mode is for simple text inputs where draft/original tracking is needed. Interactive mode delegates all keyboard handling to userland.
Focus Actions
| Action | Trigger | State Changes |
|---|---|---|
FOCUS_CELL | Click on cell | focus.target → cell |
FOCUS_HEADER | Click on header | focus.target → header |
MOVE_FOCUS | Arrow keys (navigation) | focus.target → adjacent cell |
BLUR_GRID | Focus exits grid | focus.target → null, mode → navigation |
ENTER_EDIT_MODE | Enter, F2, typing | focus.mode → edit |
EXIT_EDIT_MODE | Enter, Tab, Escape | focus.mode → navigation |
ENTER_WIDGET_MODE | Enter, F2 on composite cell | focus.mode → interactive |
EXIT_WIDGET_MODE | Escape, Tab out | focus.mode → navigation |
SYNC_CONTEXT | rowIds/colIds change | Auto-recovery if focus/selection invalid |
Grid Entry/Exit Focus
Grid is a single tab stop (W3C requirement).
| Scenario | Behavior |
|---|---|
| Tab into grid (first time) | Focus first cell (row 0, col 0) |
| Tab into grid (returning) | Focus last focused cell (remembered) |
| Tab out of grid | Focus next focusable element in document |
| Shift+Tab out of grid | Focus previous focusable element |
| Grid loses focus (blur) | If in edit mode: commit (if commitOnBlur) |
Last focused cell: The library remembers the last focused cell. When the user tabs back into the grid, focus is restored to that cell. This improves UX for keyboard navigation.
Edge Cases
| Case | Behavior |
|---|---|
| Focus on cell that no longer exists (row deleted) | Focus on nearest valid cell, or null |
| Navigate beyond boundary | No-op (focus doesn't move) |
| Empty grid | Focus null, Tab passes through |
| Focus + sorting changes row order | Focus follows rowId (stable ID) |
| Focus + mode not navigation when grid blurs | Implicit commit/exit, then blur |
Context Changes (Auto-Recovery)
When rowIds or colIds changes and the focused/selected cell becomes invalid, the library auto-recovers to maintain a valid state.
Focus Recovery
| Scenario | Recovery Behavior |
|---|---|
| Focused column removed | Focus moves to adjacent column (right → left fallback) |
| Focused row removed | Focus moves to adjacent row (below → above fallback) |
| Both row and column removed | Focus moves to nearest valid cell |
| All columns/rows removed | Focus → null |
Focus mode was edit | Implicit cancel, then recover |
Focus mode was interactive | Exit interactive, then recover |
Recovery algorithm (column removal):
1. If colIds still contains focusedColId → no action
2. Find previous colIndex of focusedColId (from previous colIds)
3. Try colIds[colIndex] (same position, new column)
4. If out of bounds, try colIds[colIndex - 1] (left)
5. If still invalid, try colIds[0] (first column)
6. If colIds is empty → focus nullImplementation note: Recovery is triggered by the SYNC_CONTEXT action, dispatched when rows/columns change. This happens automatically in useGridBehavior.
Selection Recovery
| Scenario | Recovery Behavior |
|---|---|
| Selected column removed | Selection range shrinks to exclude invalid cols |
| Anchor column removed | Anchor moves to nearest valid column |
| Entire selection invalidated | Selection cleared (ranges → []) |
| Partial invalidation | Range contracts to valid cells only |
TanStack Table Integration
When integrating with TanStack Table features (visibility, ordering, pinning), pass columns directly:
// Columns are derived reactively from TanStack Table state
const columns = table.getVisibleLeafColumns();
// useGridBehavior detects changes and dispatches SYNC_CONTEXT automatically
const grid = useGridBehavior({
rows: table.getRowModel().rows,
columns,
});This ensures:
- Hidden columns are skipped in navigation
- Reordered columns follow new order
- Pinned columns are navigated in visual order
ARIA
| Attribute | Where | Value |
|---|---|---|
tabindex="0" | Focused cell | Single tab stop |
tabindex="-1" | Other cells | Navigable via arrow |
role="grid" | Container | — |
role="gridcell" | Data cells | — |
role="columnheader" | Header cells | — |
Header Navigation
Headers are part of the navigable grid region. Arrow navigation connects headers to the data cells:
| Action | Behavior |
|---|---|
| Click on header | Focuses header (FOCUS_HEADER) |
| ArrowDown from header | Focus first data row, same column |
| ArrowUp from row-0 | Focus header in same column |
| ArrowLeft/Right on header | Navigate between headers |
| Home on header | First header |
| End on header | Last header |
| Ctrl+Home on header | First cell (row-0, col-0) |
| Ctrl+End on header | Last cell |
| ArrowUp on header | No-op (no row above headers) |
Note: Headers receive the same keyboard handler as cells, enabling consistent navigation. The onKeyDown prop from getHeaderProps delegates to the grid keyboard handler.
Generated Effects
| Action | Effects |
|---|---|
FOCUS_CELL, MOVE_FOCUS | FOCUS_ELEMENT, SCROLL_INTO_VIEW, ANNOUNCE |
FOCUS_HEADER | FOCUS_ELEMENT, SCROLL_INTO_VIEW, ANNOUNCE |
BLUR_GRID | None |
Selection
Responsibility
Tracks which cells are selected. Handles click, Shift+click, Ctrl+A, Shift+Arrow.
State
selection: {
ranges: SelectionRange[]
anchor: CellRef | null
}
SelectionRange = { start: CellRef, end: CellRef }ranges: v1 supports single contiguous range (array for future extensibility)anchor: starting point for extension with Shift
Focus vs Selection
Separate concepts. A cell can be:
- Focused but not selected (navigation without selection)
- Selected but not focused (multi-selection)
- Both (active cell in selection)
- Neither
| Scenario | Focus | Selection |
|---|---|---|
| Click on cell | Cell | Cell (single) |
| Shift+Click | Clicked cell | Range from anchor to clicked |
| Arrow without Shift | New cell | Unchanged or cleared (config) |
| Context menu open | Menu | Persists on cells |
| Focus exits grid | null | Persists (design choice) |
Rationale for selection persistence:
- Consistent with Excel/Google Sheets
- Context menu on selection: focus goes to menu, actions operate on selection
- Copy/paste works even after blur
- Explicit clear via Escape or click elsewhere
W3C Keyboard
| Key | Action |
|---|---|
Shift+→ | Extend selection right |
Shift+← | Extend selection left |
Shift+↓ | Extend selection down |
Shift+↑ | Extend selection up |
Ctrl+A | Select all cells |
Shift+Space | Select current row |
Ctrl+Space | Select current column |
Escape | Clear selection (if not in edit) |
Actions
| Action | Trigger | Transition |
|---|---|---|
SELECT_CELL | Click | ranges → single cell, anchor → cell |
EXTEND_SELECTION | Shift+Click, Shift+Arrow | ranges → range from anchor to target |
SELECT_ROW | Shift+Space | ranges → all cells in row |
SELECT_COLUMN | Ctrl+Space | ranges → all cells in column |
SELECT_ALL | Ctrl+A | ranges → all cells |
CLEAR_SELECTION | Escape, Click elsewhere | ranges → [], anchor → null |
Drag Selection (Mouse)
Selection via click + drag.
| Phase | Event | Behavior |
|---|---|---|
| Start | mousedown on cell | anchor = cell, start tracking |
| Move | mousemove (while dragging) | Calculate cell under cursor, ranges = [anchor → current] |
| End | mouseup | Stop tracking |
Implementation note: Drag tracking is userland's responsibility, not library state.
The library only receives EXTEND_SELECTION actions. It doesn't care why they're dispatched (Shift+Click, Shift+Arrow, or drag). Userland implements the mouse event loop:
// Userland implementation
const isDraggingRef = useRef(false);
const handleMouseDown = (cell: CellRef) => {
isDraggingRef.current = true;
dispatch({ type: "SELECT_CELL", cell });
};
const handleMouseMove = (cell: CellRef) => {
if (isDraggingRef.current) {
dispatch({ type: "EXTEND_SELECTION", target: cell });
}
};
const handleMouseUp = () => {
isDraggingRef.current = false;
};Auto-scroll during drag: Userland responsibility.
Edge Cases
| Case | Behavior |
|---|---|
| Selection includes deleted cell | Range shrinks to valid cells |
| Extend beyond grid boundary | Stop at boundary |
| Select all on empty grid | ranges = [] |
| Shift+Click without anchor | anchor = current focus, then extend |
| Column removed from selection | Range contracts, anchor adjusts if needed |
| Row removed from selection | Range contracts, anchor adjusts if needed |
ARIA
| Attribute | Where | Value |
|---|---|---|
aria-selected="true" | Selected cells | — |
aria-selected="false" | Unselected cells (if selection enabled) | — |
aria-multiselectable="true" | Grid | If range selection enabled |
Generated Effects
| Action | Effects |
|---|---|
| Any selection change | ANNOUNCE (if a11y enabled) |
SELECT_CELL | Also FOCUS_ELEMENT (selection moves focus) |
Editing (Edit Mode Details)
Note: Editing is now part of the unified focus mode system. This section provides additional details specific to edit mode. See Focus & Interaction Modes for mode transitions.
Prerequisites
- Cell must be focused (
focus.targetpoints to cell) - Cell must be editable (
isEditable(cell) === true) - Current mode must be
navigation(cannot enter edit from interactive)
Entering Edit Mode
| Trigger | Initial Draft Value |
|---|---|
| Enter key | Current cell value |
| F2 key | Current cell value |
| Printable character | The typed character |
| Double-click | Current cell value |
When entering edit mode:
focus.mode→'edit'draft→ initial value (current or typed char)original→ current cell value (for cancel/restore)
Exiting Edit Mode
| Trigger | Variant | Behavior | Effect Generated |
|---|---|---|---|
| Enter | Commit | Save draft, stay on cell | COMMIT_VALUE |
| Tab | Commit | Save draft, move right | COMMIT_VALUE |
| Shift+Tab | Commit | Save draft, move left | COMMIT_VALUE |
| Escape | Cancel | Discard draft, stay on cell | None |
| Click elsewhere | Commit | Save draft, focus new cell | COMMIT_VALUE |
| Blur (if configured) | Commit | Save draft | COMMIT_VALUE |
Validation
Sync Validation (v1)
| Phase | Behavior |
|---|---|
| During edit | Optional: live validation, show errors |
| Pre-commit | validate(draft) called synchronously |
| If valid | Commit proceeds |
| If invalid | Commit blocked, errors shown, stays in edit |
Validation Edge Cases
| Case | Behavior |
|---|---|
| Tab with invalid value | Commit blocked, focus stays, error shown |
| Click elsewhere with invalid value | Commit blocked, focus stays, error shown |
| Escape with invalid value | Cancel allowed, reverts to original (valid) |
| Validation throws | Treated as invalid, error message shown |
Actions
| Action | Trigger | State Changes |
|---|---|---|
ENTER_EDIT_MODE | Enter, F2, typing | mode → edit, draft/original set |
UPDATE_DRAFT | Typing in editor | draft updated |
EXIT_EDIT_MODE | Enter, Tab, Esc | mode → navigation, draft/original → null |
Implicit Commit
When the user performs navigation actions while in edit mode:
| Action | Behavior |
|---|---|
| Arrow key pressed | Pass through to input |
MOVE_FOCUS dispatched | Implicit commit, then move |
| Click on different cell | Implicit commit, focus new cell |
Note: Arrow keys during edit mode are passed to the input element for cursor movement. They do NOT trigger MOVE_FOCUS. To navigate away, use Tab or click.
Edge Cases
| Case | Behavior |
|---|---|
| Enter edit on non-editable cell | Action ignored (precondition not met) |
| Cell deleted during edit | Implicit cancel, focus invalidated |
onCommit throws | Error bubbles up |
| Enter edit when already in edit | No-op |
| Enter edit when in interactive mode | Not allowed (must exit interactive first) |
ARIA for Editing
| Attribute | Where | Value |
|---|---|---|
aria-readonly="false" | Editable cells | — |
aria-readonly="true" | Non-editable cells | — |
aria-invalid="true" | Cell with validation error | During edit |
Generated Effects
| Action | Effects |
|---|---|
EXIT_EDIT_MODE (commit) | COMMIT_VALUE, ANNOUNCE |
EXIT_EDIT_MODE (cancel) | ANNOUNCE |
| Validation error | ANNOUNCE (error message) |
Clipboard
Responsibility
Handles copy (Ctrl+C) and paste (Ctrl+V) of selected cells.
Copy
| Trigger | Behavior |
|---|---|
Ctrl+C / Cmd+C | Serialize selected cells |
Default serialization:
text/plain: values separated by tab and newline (TSV)text/html: HTML table (for paste in Excel/Sheets)
Userland can override serialization via config.
Paste
| Trigger | Behavior |
|---|---|
Ctrl+V / Cmd+V | Parse clipboard, emit event |
Behavior:
- Read clipboard (
text/plainortext/html) - Parse into matrix of values
- Emit
PASTE_DATAeffect with{ startCell, data: string[][] } - Userland decides what to do (update cells, insert rows, etc.)
Library does not modify data directly — emits event, userland applies.
Actions
| Action | Trigger | Effects |
|---|---|---|
COPY | Ctrl+C | WRITE_CLIPBOARD |
PASTE | Ctrl+V | PASTE_DATA |
DELETE | Delete, Backspace | DELETE_VALUES |
Edge Cases
| Case | Behavior |
|---|---|
| Copy without selection | Copy focused cell |
| Paste area larger than grid | Truncate to boundary (or userland extends) |
| Empty clipboard | No-op |
| Clipboard inaccessible (permissions) | Silent no-op |
Config Summary
| Option | Default | Description |
|---|---|---|
navigation.wrap | false | Arrow wraps at row end |
navigation.pageSize | 10 | Rows per Page Up/Down |
selection.mode | 'range' | 'single' or 'range' |
selection.clearOnNavigation | false | Clear selection on arrow without Shift |
editing.commitOnBlur | true | Commit when focus exits |
editing.startOnType | true | Typing enters edit |
a11y.announceNavigation | true | Announce focus changes |
a11y.announceSelection | true | Announce selection changes |
Touch Support
Out of Scope for v1. Touch devices are not officially supported.
Limitations
Touch devices lack capabilities that the library relies on:
| Desktop Feature | Touch Limitation | Impact |
|---|---|---|
Ctrl+Click | Not available | No multi-select toggle |
Shift+Click | Works but awkward | Range selection difficult |
| Hover | Not available | No column menu on hover |
| Right-click | Long-press (inconsistent) | Context menu unreliable |
| Physical keyboard | Virtual keyboard | Lifecycle issues during editing |
Workarounds for Userland
If you need touch support, consider:
| Need | Workaround |
|---|---|
| Multi-select | Add checkbox column for row selection |
| Context menu | Add explicit menu button in row |
| Column actions | Add action buttons in header |
| Virtual keyboard | Handle blur events carefully, use inputmode |
Screen Reader Compatibility
Supported Screen Readers
| Screen Reader | Platform | Compatibility | Notes |
|---|---|---|---|
| NVDA | Windows | Primary | Strict ARIA interpretation |
| JAWS | Windows | Primary | More lenient, some quirks |
| VoiceOver | macOS/iOS | Secondary | No application/forms modes |
| Narrator | Windows | Untested | — |
| TalkBack | Android | Out of scope | Touch devices not supported |
Behavior Differences
| Behavior | NVDA | JAWS | VoiceOver |
|---|---|---|---|
| Grid role recognition | Strict | Lenient | Strict |
| Application mode trigger | role="grid" | role="grid" | Focus on grid |
| Cell position announcement | "row X, column Y" | "row X of N, col Y of M" | "row X, column Y" |
| Selection announcement | On aria-selected | On aria-selected | Less consistent |
| Edit mode announcement | On focus change | On focus change | On focus change |
Testing Requirements
Before release, test these scenarios:
- Navigation: Arrow key navigation announces cell content and position
- Selection: Selection changes are announced
- Editing: Enter/exit edit mode announced
- Validation: Errors announced via live region
- Headers: Column names announced when navigating
Announcements via Live Region
The library uses a visually-hidden live region for announcements:
<div aria-live="polite" aria-atomic="true" class="visually-hidden" />| Event | Announcement Example |
|---|---|
| Focus cell | "{content}, row {n} of {total}, column {name}" |
| Enter edit mode | "Editing {content}" |
| Exit edit mode (commit) | "Saved" |
| Exit edit mode (cancel) | "Cancelled" |
| Selection change | "{n} cells selected" |
| Validation error | "Error: {message}" |
Throttling: Rapid announcements (e.g., during keyboard navigation) are throttled to avoid overwhelming the screen reader.
Keyboard Trap Prevention
W3C Requirement
"Users must be able to navigate away from any component using the keyboard."
Escape Sequence
The Escape key always provides an exit path:
| Current State | Escape Behavior |
|---|---|
| Navigation mode | Clear selection |
| Edit mode | Cancel edit → navigation mode |
| Interactive mode | Exit interactive → navigation mode |
| Interactive with popup open | Close popup → still in interactive mode |
| Interactive, popup closed | Exit interactive → navigation mode |
Double-Escape pattern: If an interactive has a popup (e.g., dropdown menu), first Escape closes the popup, second Escape exits interactive mode.
Tab Exit
From navigation mode, Tab always exits the grid:
| Condition | Tab Behavior |
|---|---|
| Navigation mode | Exit grid → next focusable |
| Edit mode | Commit → move to next cell (in grid) |
| Interactive mode (last element) | Exit interactive → move to next cell |
To exit the grid from edit mode, user must first exit edit (Enter or Escape), then Tab.
Userland Responsibilities
When implementing custom interactives in cells:
- Always handle Escape: Close any popups, then return focus to cell
- Tab should cycle or exit: Don't trap Tab within a single interactive
- Announce state changes: Use library's announce mechanism
- Test with screen readers: Ensure navigation is not blocked
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Modal that steals focus | User can't navigate away | Use non-modal popover |
| Interactive that consumes all keys | User trapped in interactive | Let Escape always exit |
| Auto-focus input that re-focuses | User can't Tab out | Only focus once on mount |
| Infinite Tab cycle | Tab never exits component | Exit on last element |
Selection During Edit Mode
Behavior
Selection is orthogonal to editing. While in edit mode:
| Aspect | Behavior |
|---|---|
| Existing selection | Persists (visually and in state) |
| Selection modification | Blocked (Shift+Arrow not allowed) |
| Clipboard operations | Operate on selection, not edit cell |
Rationale: Consistent with Excel/Google Sheets. User can copy a selection even while editing a different cell.
Edge Cases
| Scenario | Behavior |
|---|---|
| Edit cell A, Ctrl+C | Copy selection (may not include A) |
| Edit cell A, selection is [B,C] | Selection persists, A not in selection |
| Exit edit, selection still exists | Selection remains [B,C] |
| Edit cell in selection | Selection unchanged |
Virtualization Edge Cases
Overscan Buffer
When using virtualization, maintain at least 1 row/column overscan:
| Overscan | Problem |
|---|---|
| 0 | Arrow navigation to next cell fails (cell not in DOM) |
| 1 | Minimum for navigation; may flash on fast scroll |
| 2-3 | Recommended; smooth navigation |
| 10+ | Unnecessary; performance impact |
Focus and Virtualization
| Scenario | Behavior |
|---|---|
| Focus cell in viewport | FOCUS_ELEMENT effect focuses it |
| Focus cell outside viewport | Ref is null; focus silently skipped |
| Navigate to cell outside | SCROLL_INTO_VIEW first, then focus |
| Cell scrolls out during edit | Implicit cancel (cell no longer in DOM) |
Scroll-Then-Focus Pattern
When navigating to a cell outside the viewport:
- Action dispatched (e.g.,
MOVE_FOCUSwith PageDown) - Transition calculates new target cell
- Effects generated:
SCROLL_INTO_VIEW, thenFOCUS_ELEMENT - Userland intercepts
SCROLL_INTO_VIEW→ virtualizer scrolls - React re-renders with new visible cells
FOCUS_ELEMENTexecutes (cell now in DOM)
Timing: FOCUS_ELEMENT must wait for DOM update. The library uses requestAnimationFrame for this.
Fast Scrolling
During momentum scroll or rapid arrow key presses:
| Problem | Mitigation |
|---|---|
| Headers misalign with content | Sync scroll positions in RAF |
| Focus trails behind scroll | Debounce rapid navigation? |
| Cells flash empty | Increase overscan buffer |
Userland is responsible for virtualizer performance tuning.