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).

Highlight
Fig 1.1 · Grid · Designer view
Designer

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
Designer

Token usage per slot

caption
color
  • foregroundcolor.text.primary
typography
  • sizetext.md
  • weightweight.semibold
column-header
color
  • foregroundcolor.text.primary
typography
  • sizetext.sm
  • weightweight.semibold
cell
typography
  • sizetext.sm
Both

Figma ↔ Code property map

FigmaKindCodeNotes
VariantEnumvariantMaps static / editable. Editable activates F2/Enter/Escape edit-mode lifecycle and cell-editor slot mounting.
SizeEnumsizesm / md.
Multi SelectBooleanmultiSelectWhen true, supports multi-cell selection via Shift+Arrow / Ctrl+Click; root carries `aria-multiselectable="true"`.
VirtualizedBooleanvirtualizedWhen true, only viewport-visible rows render; requires `aria-rowcount` / `aria-colcount` on root and `aria-rowindex` / `aria-colindex` per rendered cell.
Sticky HeaderBooleanstickyHeaderWhen true, column-headers-row uses `position: sticky` so headers stay visible during vertical scroll.
SortableBooleansortablePer-column sort affordance plus `aria-sort` on column headers.
Row CountNumberrowCountTotal row count (for virtualized grids); reflects to `aria-rowcount`. Omitted for non-virtualized grids — implicit DOM count suffices.
Column CountNumbercolCountTotal column count (for virtualized grids); reflects to `aria-colcount`. Omitted for non-virtualized grids.
Designer

Motion

TransitionDuration token
cellFocusTransitionmotion.duration.fast
editModeEntermotion.duration.fast
selectionExpandmotion.duration.fast
Easing
motion.easing.standard
Reduced motion
Instant (jump cut)
Designer

Responsive behaviour

BreakpointChange
breakpoint.smBelow 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.mdAt 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.
Both

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`.

Both

Variants, properties, states

Variants

Structurally different versions of the component.

static editable

Properties

The same component, parameterised.

PropertyType
size sm | md
multiSelect boolean
virtualized boolean
stickyHeader boolean
sortable boolean

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
hoverfocus-visibleactive
data
idleeditinghasSelectionhasSortingloading
Both

State transitions

FromToTrigger
idleeditingUser presses F2 or Enter on a focused editable cell (editable variant only)
editingidleUser presses Enter (commit) or Escape (cancel) inside the cell-editor
idlehasSelectionUser activates selection via Space (single-cell), Shift+Arrow (range), or Ctrl+Click (multi)
idlehasSortingUser activates a sortable columnheader via Enter / Space
idleloadingAsync fetch begins (server-side sort, filter, or pagination)
loadingidleAsync fetch resolves with new data
Both

Figma↔Code mismatches

  1. 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

    Consequence

    Designers 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.

    Correct

    Document 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.

  2. 02
    Figma

    Edit-mode drawn as a separate frame replacing the cell

    Code

    In-place input replacement — cell-editor slot mounts inside the cell, not in a separate region

    Consequence

    Designers 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.

    Correct

    Document 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.

  3. 03
    Figma

    Selection highlight drawn as solid background on selected cells

    Code

    Visual treatment plus `aria-selected="true"` on the cell or row

    Consequence

    Designers 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.

    Correct

    Document 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"`.

  4. 04
    Figma

    Header row drawn identically to data rows

    Code

    `role="columnheader"` / `role="rowheader"` semantics distinct from `role="gridcell"`

    Consequence

    Designers 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.

    Correct

    Document 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.

  5. 05
    Figma

    Grid drawn without explicit roving-tabindex documentation

    Code

    Single tab-stop into grid; arrow keys move focus between cells

    Consequence

    Designers 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.

    Correct

    Document 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.

Both

Contracts

Non-negotiable contracts

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.
Designer

Common mistakes

Blocker

#grid-no-roving-tabindex

Every cell is `tabindex="0"` (or no cells are)

Problem

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.

Fix

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.

Blocker

#grid-arrow-no-cell-focus

Arrow keys do not move focus between cells

Problem

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"`.

Fix

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.

Major

#grid-edit-mode-no-escape

Escape does not cancel edit-mode

Problem

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.

Fix

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.

Major

#grid-cell-not-gridcell-role

Cells use `<td>` without `role="gridcell"` when root is `role="grid"`

Problem

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.

Fix

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.

Major

#grid-no-aria-rowcount-virtualized

Virtualized grid omits `aria-rowcount` and `aria-rowindex`

Problem

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.

Fix

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.

Minor

#grid-when-table-suffices

Grid pattern used when Table would work

Problem

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.

Fix

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.