Designer view
Grid
An interactive 2D data structure where cells themselves are focusable — the APG escalation from `Table` (read-only data display) for surfaces where users navigate cell-by-cell, edit in place, or select rectangular regions. Carries `role="grid"` on the root, `role="row"` on rows, and `role="gridcell"` / `role="columnheader"` / `role="rowheader"` on cells. Keyboard model: roving-tabindex (one cell holds the tab stop), 2D arrow navigation, Home/End/Ctrl+Home/Ctrl+End for boundary jumps, F2 / Enter to enter edit-mode, Escape to cancel.
Also called Data grid Interactive grid Editable grid
When to use
Use
For tabular surfaces where cells are themselves the interactive units — spreadsheets, in-line-editable inventories, dense data-entry surfaces, focus-cell-driven data visualisations (cohort heatmaps with cell-level drilldown). Use the static variant when cells are interactive but not editable (clickable cells that open detail panels); use the editable variant when cells transition to input-mode for value editing. Pair with pagination or virtualization for large datasets; canonical-grid implementations support both via the `aria-rowcount` / `aria-colcount` attributes.
Avoid
For read-only tabular data where users scan and compare attributes via standard tab order through interactive descendants — that is `Table` (simpler keyboard model, less authoring overhead). For hierarchical rows with parent-child relationships and per-row expand / collapse — that is `TreeGrid`. For form-style input grids where each cell is a labeled form control — use a real form with grouped inputs, not Grid; the grid pattern is for cells-as-data, not cells-as-form-fields. For non-tabular 2D layouts (image galleries, dashboards) — those are CSS Grid layouts, not the WAI-ARIA grid pattern.
Versus related
- table
`Table` is read-only data display with linear focus through interactive descendants (links, buttons, checkboxes inside cells); standard Tab order navigates between focusables. `Grid` makes cells themselves focusable with 2D arrow-navigation and roving-tabindex (one tab-stop into the grid). Use Table when the cell content is interactive but the cells themselves are not (a "Delete" button per row); use Grid when cells are the unit of interaction (spreadsheet-style cell focus, rectangular selection, in-place edit). The escalation cost is real — Grid's keyboard model requires roving-tabindex bookkeeping plus F2 / Enter / Escape edit-lifecycle handling that Table avoids.
- tree-grid
`TreeGrid` adds hierarchical row semantics on top of the Grid keyboard model — `aria-level` per row, `aria-expanded` on parent rows, ArrowRight expands a collapsed parent / moves into first child, ArrowLeft collapses / moves to parent. Use TreeGrid for org charts, file managers with column metadata, nested budgets where rows have parent-child relationships; use Grid for flat tabular data with interactive cells.
- combobox
`Combobox` is a single-input affordance with a listbox of selectable options (one-dimensional linear list); `Grid` is a 2D structure with cells in rows and columns (two-dimensional spatial navigation). Combobox suits "pick one from a list"; Grid suits "navigate cell-by-cell through structured data". Their keyboard models diverge — Combobox uses ArrowDown / ArrowUp for list traversal, Grid uses all four arrows for cell navigation.
Grid is the APG-canonical pattern for interactive tabular data — spreadsheets, editable inventories, focus-cell-rich data visualisations. Distinct from `Table` (the canonical read-only data-display pattern with linear focus through interactive descendants) and from `TreeGrid` (hierarchical rows with `aria-expanded` per parent). The reference documents the roving-tabindex contract (single tab-stop into the grid; arrows move focus between cells), the F2 / Enter / Escape edit-mode lifecycle, the `aria-rowcount` / `aria-rowindex` / `aria-colcount` / `aria-colindex` rules for virtualized grids, and the boundary with Table (escalate from Table to Grid only when interactive cells justify the steeper keyboard model).
Figma anatomy
| Slot | Figma type | Hint |
|---|---|---|
root | frame | Grid frame; variant property switches between static and editable shapes |
caption | text | Caption text style; positioned above the grid; visibility per "Visible Caption" property |
column-headers-row | frame | Header row container; sticky-position variant per "Sticky Header" property |
column-header | instance | Column header cell instance with sort-affordance + resize-handle variants |
row | instance | Row instance with selected / hover / focused-cell variants |
row-header | instance | Row header cell instance; structural variant of cell with rowheader role |
cell | instance | Data cell instance with default / focused / selected / editing / readonly variants |
cell-editor | instance | Cell editor instance — input / select / textarea variants per cell-data-type |
selection-overlay | frame | Decorative overlay rectangle; absolute-positioned over selected cell range |
Token usage per slot
caption- color
- foreground
color.text.primary
- foreground
- typography
- size
text.md - weight
weight.semibold
- size
column-header- color
- foreground
color.text.primary
- foreground
- typography
- size
text.sm - weight
weight.semibold
- size
cell- typography
- size
text.sm
- size
Figma ↔ Code property map
| Figma | Kind | Code | Notes |
|---|---|---|---|
Variant | Enum | variant | Maps static / editable. Editable activates F2/Enter/Escape edit-mode lifecycle and cell-editor slot mounting. |
Size | Enum | size | sm / md. |
Multi Select | Boolean | multiSelect | When true, supports multi-cell selection via Shift+Arrow / Ctrl+Click; root carries `aria-multiselectable="true"`. |
Virtualized | Boolean | virtualized | When true, only viewport-visible rows render; requires `aria-rowcount` / `aria-colcount` on root and `aria-rowindex` / `aria-colindex` per rendered cell. |
Sticky Header | Boolean | stickyHeader | When true, column-headers-row uses `position: sticky` so headers stay visible during vertical scroll. |
Sortable | Boolean | sortable | Per-column sort affordance plus `aria-sort` on column headers. |
Row Count | Number | rowCount | Total row count (for virtualized grids); reflects to `aria-rowcount`. Omitted for non-virtualized grids — implicit DOM count suffices. |
Column Count | Number | colCount | Total column count (for virtualized grids); reflects to `aria-colcount`. Omitted for non-virtualized grids. |
Motion
| Transition | Duration token |
|---|---|
cellFocusTransition | motion.duration.fast |
editModeEnter | motion.duration.fast |
selectionExpand | motion.duration.fast |
Responsive behaviour
| Breakpoint | Change |
|---|---|
breakpoint.sm | Below this width, the grid keyboard model becomes hard to use on touch (no arrow keys on virtual keyboards, no F2 to enter edit-mode). The canonical pattern is to escalate to a Table read-only view on narrow viewports — collapse cells to stacked-card-per-row, lose the cell-focus interaction. Editable grids on mobile route through tap-to-edit modal flows rather than in-place edit-mode. |
breakpoint.md | At and above this width, the full grid keyboard model is active — arrow keys navigate cells, F2 / Enter enter edit- mode, Escape cancels. Sticky-header and sticky-first-column activate when the grid exceeds the viewport. |
Internationalisation
RTL · mirroring
Column flow follows logical direction — leading column (typically row-identity) sits at the inline-start, which flips between LTR and RTL. Arrow-key navigation flips accordingly: ArrowRight under RTL moves to the inline-end column (visually left); APG explicitly recommends mapping arrow keys to the visual direction so the keyboard model stays intuitive in both writing systems. Sticky-first-column logic uses `inset-inline-start: 0`. Numeric cells text-align logical-end; text cells logical-start.
Text expansion
Column headers expand 30-50% under translation ("Quantity" → "Hoeveelheid" 100% longer in Dutch). Long headers force wider columns and may push the grid past viewport width — the scrollable-region wrapper (inherited from the Table contract) absorbs this. Cell content expands similarly; cell-editor inputs need flexible sizing so long values do not truncate on edit-mode entry. Numeric cells stable across locales via `Intl.NumberFormat`.
Variants, properties, states
Variants
Structurally different versions of the component.
static editable Properties
The same component, parameterised.
| Property | Type |
|---|---|
size | sm | md |
multiSelect | boolean |
virtualized | boolean |
stickyHeader | boolean |
sortable | boolean |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | hoverfocus-visibleactive |
data | idleeditinghasSelectionhasSortingloading |
State transitions
| From | To | Trigger |
|---|---|---|
idle | editing | User presses F2 or Enter on a focused editable cell (editable variant only) |
editing | idle | User presses Enter (commit) or Escape (cancel) inside the cell-editor |
idle | hasSelection | User activates selection via Space (single-cell), Shift+Arrow (range), or Ctrl+Click (multi) |
idle | hasSorting | User activates a sortable columnheader via Enter / Space |
idle | loading | Async fetch begins (server-side sort, filter, or pagination) |
loading | idle | Async fetch resolves with new data |
Figma↔Code mismatches
- 01 Figma
Cells drawn as static text without focus-affordance
Code`role="gridcell" tabindex="-1"` (or `tabindex="0"` for the active cell) plus 2D arrow-key handler
ConsequenceDesigners compose grid cells as static text in a tabular layout; developers must add the roving- tabindex keyboard model so cells are focusable. Without explicit anatomy, the focus-affordance is invisible in the design file — implementations may ship the visual without the keyboard model and AT users get standard tab-through behaviour that is not the canonical grid pattern. Sighted- mouse users notice nothing; keyboard + AT users lose the cell-by-cell navigation contract.
CorrectDocument each cell as an interactive slot with explicit focus-state (default / focused / selected / editing). Figma carries a "focused" variant of the cell instance with the canonical focus outline + roving-tabindex semantic noted in the anatomy. Code wires `tabindex="-1"` on inactive cells, `tabindex="0"` on the active cell, and arrow-key handlers that update which cell holds the active tab-stop.
- 02 Figma
Edit-mode drawn as a separate frame replacing the cell
CodeIn-place input replacement — cell-editor slot mounts inside the cell, not in a separate region
ConsequenceDesigners compose edit-mode as a modal-like separate frame ("Edit Cell" panel beside or below the grid); developers ship in-place input replacement (the cell becomes an `<input>` in the same DOM position). The two surfaces diverge structurally — designer's separate frame loses the spatial context of the cell, developer's in-place edit preserves it. Without explicit anatomy, the edit-mode pattern is ambiguous and implementations may ship either.
CorrectDocument the cell-editor slot as a child of the cell, not a separate region. Figma carries an "editing" variant of the cell instance with the editor input mounted inside the cell's bounding box. Code mounts `<input>` / `<select>` inside the cell's `<td>` element on edit-mode entry; the cell's role and ARIA attributes shift to delegate-to-input semantics. Modal-style edit panels are a separate pattern (escalation to a Modal or Drawer for bulk-edit); in-place is the canonical Grid edit-mode shape.
- 03 Figma
Selection highlight drawn as solid background on selected cells
CodeVisual treatment plus `aria-selected="true"` on the cell or row
ConsequenceDesigners compose selection as a solid background color on selected cells; developers ship the same visual plus `aria-selected="true"` so AT users hear "selected". Without explicit anatomy, the AT-semantic may ship without `aria-selected` and the visual works for sighted users only. Multi-cell rectangular selection in particular tends to ship as visual-only because the multi-cell pattern is complex.
CorrectDocument selection-overlay as a decorative slot AND `aria-selected` per cell / row as a contract. Figma carries the selection visual treatment as a state of the cell instance; the design surface signals "this visual reflects a programmatic state". Code sets `aria-selected="true"` on every selected cell or row alongside the visual treatment. For multi-cell rectangular selection, every cell in the rectangle carries `aria-selected="true"`.
- 04 Figma
Header row drawn identically to data rows
Code`role="columnheader"` / `role="rowheader"` semantics distinct from `role="gridcell"`
ConsequenceDesigners compose grids with header rows styled distinctly (bolder text, sunken background) but no anatomy slot for the header semantic; developers must wire `role="columnheader"` / `role="rowheader"` so AT users hear the header context. Without explicit anatomy, headers may ship as plain `gridcell` and the data-cell-to-header association breaks programmatically.
CorrectDocument column-header and row-header as distinct interactive slots from cell. Figma carries column-header and row-header as separate component instances with their own visual treatments; the design surface signals the role-distinction. Code wires `<th role="columnheader" scope="col">` for column headers, `<th role="rowheader" scope="row">` for row headers, and `<td role="gridcell">` for data cells.
- 05 Figma
Grid drawn without explicit roving-tabindex documentation
CodeSingle tab-stop into grid; arrow keys move focus between cells
ConsequenceDesigners compose grids as visual-only tabular surfaces without surfacing the roving-tabindex contract; developers must implement the keyboard model (one `tabindex="0"` cell at a time, arrow keys shift it, Tab exits the grid). Without explicit anatomy, implementations may ship every cell as `tabindex="0"` (every cell is a tab-stop, totally broken keyboard model) or no cells as `tabindex="0"` (cells unreachable via keyboard). The roving- tabindex contract is the highest-load-bearing design-vs-code translation.
CorrectDocument the roving-tabindex contract as a first-class anatomy slot property. Figma carries the focused-cell variant explicitly with annotations indicating the tab-stop semantic. Code wires roving- tabindex bookkeeping on every arrow-key navigation — the cell receiving focus gains `tabindex="0"`, the cell losing focus drops to `tabindex="-1"`. Tab from outside enters the grid at the active cell; Tab inside the grid exits to the next document focusable.
Contracts
Non-negotiable contracts
APGAPG: Grid pattern keyboard interaction The grid implements roving-tabindex — only one cell carries `tabindex="0"` at a time; all other cells carry `tabindex="-1"`. Tab from outside enters the grid at the active cell; Tab inside the grid exits to the next document focusable. Implementations that ship every cell as `tabindex="0"` or no cells as `tabindex="0"` violate the APG grid keyboard model.
Without roving-tabindex, the grid is either a Table-with-broken-tab-model (every cell a tab stop, breaking the single-tab-into-grid contract) or a keyboard-trap (no cells reachable). The APG grid pattern's whole point is the roving-tabindex keyboard model; without it, `role="grid"` is misleading.
APGAPG: Grid pattern keyboard interaction Arrow keys move focus between cells — ArrowUp / ArrowDown navigate rows, ArrowLeft / ArrowRight navigate columns. Home / End jump within row; Ctrl+Home / Ctrl+End jump to grid boundaries; Page Up / Page Down move by viewport-row-count.
Without 2D arrow-key navigation, the grid pattern degrades to a Table with misleading role. Keyboard users cannot navigate cell-by-cell; the canonical affordance disappears.
APGAPG: Grid pattern editable cells interaction Editable variant supports F2 / Enter to enter edit-mode, Escape to cancel (revert + exit), Enter to commit (commit + exit), Tab to commit-and- advance. The three exits are the canonical edit-mode contract.
Without the three-exit contract, users get trapped in the cell-editor (no Escape) or commit changes accidentally (no Tab-to-commit-and-advance) or cannot enter edit-mode at all (no F2 / Enter handler). The canonical edit- lifecycle is the difference between usable and unusable editable grids.
APGWAI-ARIA grid role and required children Cells inside `role="grid"` carry `role="gridcell"` (data cells), `role="columnheader"` (column headers), or `role="rowheader"` (row headers). The role hierarchy is consistent — grid contains rows, rows contain cells with the canonical cell roles.
Without the role hierarchy, AT announces inconsistent semantics — root as grid but cells as plain table cells. SR users hear "grid, 5 rows, 7 columns" on entry but "cell, 12" without the gridcell role on navigation. The role mismatch breaks programmatic relationships AT relies on.
APGAPG: Grid pattern virtualization guidance Virtualized grids declare `aria-rowcount` and `aria-colcount` on the root and `aria-rowindex` / `aria-colindex` per rendered cell. The virtualizer keeps these in sync as it recycles DOM nodes.
Without the explicit indices, AT users perceive the dataset as the rendered count (50 of 10,000); Page-Down navigation becomes unreliable as the virtualizer recycles nodes. The virtualization-aware ARIA attributes are the canonical bridge between the rendered subset and the logical structure.
Vocabulary drift
- WAI-ARIA
role=grid + role=gridcell + role=row- Canonical ARIA pattern. WAI-ARIA Authoring Practices Guide documents the full keyboard model including roving- tabindex, 2D arrow navigation, F2 / Enter / Escape edit-mode lifecycle. The canonical anatomy mirrors APG directly.
- HTML
<table role="grid">- Native HTML `<table>` plus explicit `role="grid"` is the canonical hybrid — retains the structural HTML semantics while signaling the interactive keyboard model. `<div role="grid">` is the fallback for non-tabular layouts.
- AG Grid
AgGridReact / AgGridAngular / AgGridVue- AG Grid is the most common production grid library across React / Angular / Vue. Implements roving-tabindex, virtualization, edit-mode lifecycle, cell renderers, and editor renderers out of the box. Naming-only divergence from canonical (`columnDefs` / `rowData` props; `onCellEditingStarted` / `onCellEditingStopped` events).
- React Aria
useGrid + useGridCell + useGridRow- React Aria ships headless hooks for the Grid pattern — consumers compose the DOM and visual treatment, the hooks wire the ARIA attributes plus roving-tabindex. The grid keyboard model is implemented inside the hooks; consumers do not re-implement it.
- TanStack Table
createColumnHelper + useReactTable (read-only)- TanStack Table provides headless table logic (sorting, filtering, virtualization) but does not implement the grid keyboard model. Pair TanStack Table with React Aria's grid hooks for canonical Grid-pattern compliance, or escalate to AG Grid for full grid-pattern feature coverage.
- Material 3
— (no formal Grid pattern component)- Material 3 spec does not include the Grid pattern. Material UI ships `DataGrid` / `DataGridPro` (separate library, paid tier) implementing the canonical pattern. Angular Material's `MatTable` does not ship the grid keyboard model.
- Carbon
DataTable (with inline-edit pattern)- Carbon ships `DataTable` covering the Table pattern; for editable grids, Carbon documents an inline-edit pattern but does not ship a dedicated Grid component. Production Carbon applications tend to escalate to AG Grid for editable use cases.
- Glide Data Grid
Canvas-based DataGrid (React)- Canvas-based grid implementation (renders cells via `<canvas>` rather than DOM). High performance for very large grids (100k+ rows) but requires explicit canvas-accessibility-tree wiring for AT support. Canonical WAI-ARIA grid-pattern compliance is implementation-specific — Glide documents an `accessibility` prop that wires `aria-` attributes for the focused cell.
Common mistakes
#grid-no-roving-tabindex
Every cell is `tabindex="0"` (or no cells are)
The grid renders without roving-tabindex — either every cell carries `tabindex="0"` (creating N×M tab stops and breaking the single-tab-into-grid contract) or no cells carry `tabindex="0"` (cells unreachable via keyboard). Both patterns violate the APG grid keyboard model. Tab from outside should enter the grid at exactly one active cell; arrow keys should move focus between cells without changing the tab-stop count.
Wire roving-tabindex: only the active cell has `tabindex="0"`; all other cells have `tabindex="-1"`. On arrow-key navigation, shift the `tabindex="0"` from the leaving cell to the entering cell. Tab from outside enters the active cell; Tab inside the grid exits to the next document focusable. Most grid libraries (React Aria, AG Grid, Glide DataGrid) implement this correctly out of the box.
#grid-arrow-no-cell-focus
Arrow keys do not move focus between cells
The grid implements no arrow-key handler (or implements scroll-only behaviour like `Table`). Keyboard users cannot navigate cell-by-cell — Tab through the grid jumps between rows or exits the grid entirely; arrow keys do nothing. The whole point of the grid pattern is 2D arrow-navigation; without it, the surface is a Table with misleading `role="grid"`.
Implement arrow-key handlers — ArrowUp / ArrowDown move focus to the cell in the adjacent row, same column; ArrowLeft / ArrowRight move to adjacent column, same row. Home / End jump to the first / last cell in the row; Ctrl+Home / Ctrl+End jump to the first / last cell of the entire grid. Page Up / Page Down move by a viewport's worth of rows. The handlers shift the roving-tabindex along with the focus.
#grid-edit-mode-no-escape
Escape does not cancel edit-mode
In editable variant, the cell-editor enters on F2 / Enter but Escape does not return focus to the cell with the original value preserved. Users get trapped in the editor — Escape is the canonical "I changed my mind, revert" gesture and its absence forces users to either commit a tentative edit or hunt for an alternative cancel path.
Implement Escape inside the cell-editor: revert the cell's value to its pre-edit state, unmount the editor, return focus to the cell, exit edit-mode. Enter inside the editor commits the new value and exits; Tab inside the editor commits AND moves to the next cell. The three exits (Enter commit, Tab commit-and-advance, Escape cancel) are the canonical edit-mode contract.
#grid-cell-not-gridcell-role
Cells use `<td>` without `role="gridcell"` when root is `role="grid"`
The root element carries `role="grid"` but the cells are bare `<td>` elements without `role="gridcell"`. AT announces the root as a grid but the cells inside as plain table cells — the role hierarchy is broken. SR users hear inconsistent semantics on grid entry vs cell navigation.
When the root is `role="grid"` (or `<table role="grid">`), the cells inside MUST carry `role="gridcell"` (data cells), `role="columnheader"` (column header cells), or `role="rowheader"` (row header cells). For native `<th>` and `<td>` elements, add `role="columnheader"` / `role="gridcell"` explicitly — the implicit table semantics do not propagate through the explicit `role="grid"` override.
#grid-no-aria-rowcount-virtualized
Virtualized grid omits `aria-rowcount` and `aria-rowindex`
The grid renders only viewport-visible rows plus a buffer (50 of 10,000); without `aria-rowcount` on the root and `aria-rowindex` per row, AT reports the rendered count as the total. Page-Down navigation becomes unreliable because the virtualizer recycles DOM nodes between renders. AT users perceive the dataset as smaller than reality.
On the root set `aria-rowcount="10000"` (true total) and `aria-colcount` similarly. On each rendered row set `aria-rowindex` to the 1-based true index (not the rendered offset); on each cell set `aria-colindex` to the 1-based true column index. The virtualizer keeps these in sync as it recycles rows; AT reports the real dataset size.
#grid-when-table-suffices
Grid pattern used when Table would work
The component ships with `role="grid"` and roving-tabindex but the cells are not themselves interactive — content is read- only, no edit-mode, no cell-level selection. The added keyboard-model complexity (roving-tabindex bookkeeping, arrow-key handlers, Page-Up / Page-Down logic) buys nothing. Authors paid the cost without gaining the affordance.
Use `Table` for read-only data display. The cells can still contain interactive descendants (links, buttons, checkboxes) reachable via standard Tab order. Reserve `Grid` for surfaces where cells themselves are interactive units — editable cells, cell-by-cell selection, focus-cell-driven drilldown. The escalation cost should buy a real keyboard-model improvement.
Accessibility hints
| Slot | Accessibility hint | |
|---|---|---|
root | `<table role="grid" aria-rowcount="..." aria-colcount="..." aria-multiselectable="...">`. Native `<table>` element retains the structural HTML semantics (rowgroups, header association) while the explicit `role="grid"` signals the interactive keyboard model. For non-tabular layouts where `<table>` is not appropriate, `<div role="grid">` is the canonical fallback. AT users hear "grid, N rows, M columns" on entry. | |
caption | `<caption>` is the canonical accessible name for grids using `<table role="grid">`. For `<div role="grid">` implementations, use `aria-labelledby` pointing to a sibling heading id. SR announces the caption on grid entry. Translate per locale. | |
column-headers-row | `<tr role="row">` containing `<th role="columnheader" scope="col">` children (or `<div role="row">` + `<div role="columnheader">` for non-table grids). The columnheader role plus `scope="col"` carries the data-cell association. Columnheaders are focusable in the grid keyboard model — arrow keys navigate to them like any other cell. | |
column-header | `<th role="columnheader" scope="col" tabindex="-1">` — `tabindex="-1"` for non-active cells in the roving-tabindex scheme; the active cell carries `tabindex="0"`. `aria-sort="ascending"` / `"descending"` / `"none"` when sortable. The cell's accessible name is its text content; sortable headers should embed the column-name in a `<button>` or announce the sort affordance via `aria-describedby`. | |
row | `<tr role="row" aria-rowindex="3">` (or `<div role="row">` for non-table grids). `aria-rowindex` is required for virtualized grids; for non-virtualized, the implicit DOM position suffices. `aria-selected` on the row when row-level selection is active; row-level activation typically comes from clicking a row-checkbox or pressing Space on a focused row-header. | |
row-header | `<th role="rowheader" scope="row" tabindex="-1">`. SR announces the row-header on cell-by-cell navigation within the row, providing row context the same way columnheader provides column context. Optional — spreadsheet-style grids without semantic row identity (just numbered rows) omit this slot. | |
cell | `<td role="gridcell" tabindex="-1">` for non-active cells; the active cell has `tabindex="0"`. Activating (F2 / Enter) transitions to edit-mode in the editable variant; activating in static variant triggers the cell's onClick or no-op. `aria-colindex` for virtualized grids (1-based); `aria-selected` for selection-state; `aria-readonly="true"` for non-editable cells in editable grids. | |
cell-editor | Native `<input>` / `<select>` / `<textarea>` carries the right keyboard model and AT semantics without ARIA. On edit-mode entry, focus moves to the editor (replacing the cell's roving-tabindex slot). The editor's `<label>` is the cell's accessible name — canonically derived from the column header. On Escape, focus returns to the cell with the original value preserved. On Enter or Tab-out, the new value commits and focus returns to the cell (or the next cell on Tab). | |
selection-overlay | `aria-hidden="true"`. The overlay is visual scaffolding for sighted users; the AT semantic lives on `aria-selected` per cell and per row. Spreadsheet-style multi-cell selection rectangles use this slot for the visible bounding box; AT users hear "selected" cell-by-cell as they navigate. |