Announcing Rectangular Cell Range Selections

Permalink to "Announcing Rectangular Cell Range Selections"

A rectangular range selection is a block of cells chosen by an anchor corner and a moving corner, as in any spreadsheet. The announcement it needs is a description of the rectangle — where it starts, where it ends, how big it is — spoken once when the user stops extending it. The failure it prevents is the one most grids produce by default: either total silence, because aria-selected changes on cells that are not focused are not announced, or a flood, because the app announces every cell as it joins the selection.

This page belongs to cell selection & clipboard in data grids. The keys that grow the range are covered in extending selection with Shift and arrow keys.

Spec reference

Permalink to "Spec reference"

ARIA 1.2 allows aria-selected on gridcell, columnheader and rowheader when the grid has aria-multiselectable="true". Screen readers announce the selected state of the focused cell as the user moves; they do not announce state changes on other cells. So a user who presses Shift+Down five times hears the focused cell each time, possibly with “selected”, but never learns that the selection is now 3 columns wide.

There is no ARIA property that describes a range. The description must come from a status message (SC 4.1.3 Status Messages) — ideally a polite live region shared by the grid — composed from the anchor and focus positions.

Criteria in play: SC 4.1.2 Name, Role, Value for per-cell state; SC 4.1.3 for the range message; SC 1.3.1 for making sure the selected set is programmatic and not only a highlight.

A selected range and the message that describes it Mock grid with a three-by-three block of selected cells from Region North Q1 to Region West Q3, with anchor and focus corners numbered. A selected range and the message that describes itRegionQ1Q2Q3Q4North1201135128140South9810111096West7780912881Anchor: where Shift-selection started(North, Q1)2Focus: the moving corner (West, Q3)3Message: "North Q1 to West Q3 selected, 3columns by 3 rows"
The message names two corners and a size — it never lists the nine cells.

When to announce a range — and when not to

Permalink to "When to announce a range — and when not to"

Announce when the selection is a rectangle of two or more cells and the gesture has settled. A single-cell selection needs no message; the focused cell’s own announcement covers it.

Do not announce during the gesture. A user holding Shift+Right with key repeat generates a change every 30 milliseconds; each message would cancel the previous one. Settle with a short debounce, around 300 milliseconds, as described in debouncing status messages for bulk operations.

The misapplication to name is using aria-activedescendant tricks or temporary focus moves to make the reader “visit” every selected cell. It produces a burst of speech and moves the user’s reading position.

Annotated code example

Permalink to "Annotated code example"
// Selection state is two corners; everything else is derived
const sel = { anchor: null, focus: null };   // { row, col } in data coordinates

function rangeBounds() {
  const r1 = Math.min(sel.anchor.row, sel.focus.row), r2 = Math.max(sel.anchor.row, sel.focus.row);
  const c1 = Math.min(sel.anchor.col, sel.focus.col), c2 = Math.max(sel.anchor.col, sel.focus.col);
  return { r1, r2, c1, c2 };
}

function paintSelection() {
  const { r1, r2, c1, c2 } = rangeBounds();
  grid.querySelectorAll('[role="gridcell"]').forEach((cell) => {
    const r = +cell.dataset.row, c = +cell.dataset.col;
    // SC 4.1.2: state on every cell, not just the focused one
    cell.setAttribute('aria-selected', String(r >= r1 && r <= r2 && c >= c1 && c <= c2));
  });
}

// One message per settled gesture (SC 4.1.3)
let t;
function announceRange() {
  clearTimeout(t);
  t = setTimeout(() => {
    const { r1, r2, c1, c2 } = rangeBounds();
    const cols = c2 - c1 + 1, rows = r2 - r1 + 1;
    if (cols * rows < 2) return;                           // single cell: silent
    const start = `${rowName(r1)} ${colName(c1)}`;          // "North Q1"
    const end = `${rowName(r2)} ${colName(c2)}`;            // "West Q3"
    status(`${start} to ${end} selected, ${cols} column${cols > 1 ? 's' : ''} by ${rows} row${rows > 1 ? 's' : ''}.`);
  }, 300);
}
<!-- SC 1.3.1: the grid declares that several cells can be selected -->
<div role="grid" aria-multiselectable="true" aria-label="Quarterly sales by region">
  <!-- rows of gridcells with aria-selected="true|false" -->
</div>
<p role="status" class="visually-hidden" id="grid-status"></p>

Name the corners with header text, not internal coordinates. “North Q1 to West Q3” is meaningful in a table with row and column headers. Spreadsheet-style grids with lettered columns can use “B2 to D4”, because that is how their sighted users think about the sheet too.

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Key / event Expected announcement AT-specific deviations
Shift+Right (first) Focused cell: “Q2, 135, selected” NVDA says “selected”; JAWS may not in some modes
Shift+Down ×2 Each focused cell, then after settling: “North Q1 to West Q2 selected, 2 columns by 3 rows.” VoiceOver may cut the cell read to speak the status
Plain arrow Selection collapses; “selection cleared” optional Only announce the clear if the range was large
Ctrl+A “All 20 cells selected, 4 columns by 5 rows.” Grid must not let the browser select page text
Alt+= (optional) “Sum 1,154, average 96.2, 12 cells” On request only
A Shift-selection gesture and its single message Timeline of five Shift-arrow presses followed by a settle delay and one range announcement. A Shift-selection gesture and its single messageShift+Rightfocused cell readShift+Rightfocused cell readShift+Downfocused cell readShift+Downfocused cell read300 ms quietdebounce elapsesStatus"North Q1 to West Q3, 3 by3"one selection gesture
The per-key feedback is the focused cell; the range description arrives once, after the user stops.

Integration context

Permalink to "Integration context"

The rectangle this message describes is what the clipboard will copy, so the two features share the corner model — see keyboard copy and paste in data grids. The state painting uses per-cell aria-selected, and the choice between that and row-level selection is covered in aria-selected on cells versus rows.

In a virtualised grid, cells outside the rendered window cannot carry aria-selected. The message becomes the only complete description of the selection, which makes it more important, not less.

Naming the corners by grid type Matrix of grid types, how to name the corners of a selected range in each, and an example message. Naming the corners by grid typeGrid typeCorner namingExampleReport with headersRow and column header textNorth Q1 to West Q3SpreadsheetColumn letter + row numberB2 to D4ScheduleResource and time slotAlice 09:00 to Bob 11:00Virtualised, no headersRow and column numbersRow 12 column 2 to row 40 column 5
Use the vocabulary sighted users of the same grid already use.

Gotchas

Permalink to "Gotchas"

Direction matters to the user, not the message. Selecting upwards from West Q3 to North Q1 describes the same rectangle. Normalise to top-left and bottom-right, but keep the anchor where it was so further extension behaves correctly.

Row and column headers in the range. Selecting a column header to select a whole column is common. Announce it as “Q2 column selected, 3 cells” rather than as corners.

Aggregates on every change. Spreadsheets show sum and count in a status bar. Speaking them after every gesture is noise; offer a key to read them.

Testing checklist

Permalink to "Testing checklist"

FAQ

Permalink to "FAQ"
How long should the debounce before a range announcement be?

Around 300 milliseconds works for most users: longer than key-repeat intervals, so a held Shift+Arrow produces one message, and short enough that the description follows the gesture without a noticeable gap.

Why doesn't my screen reader announce cells being added to a selection?

Screen readers announce the state of the focused element, not state changes on other elements. When Shift+arrow extends a range, only the newly focused cell is read. Describe the whole range with a status message after the gesture settles.

What should a range selection announcement say?

The two corners, named by their headers or spreadsheet coordinates, and the size — for example “North Q1 to West Q3 selected, 3 columns by 3 rows” — spoken once when the user stops extending the selection.

Should I use aria-selected on every cell in the range?

Yes, when the grid has aria-multiselectable=“true”, so the state is programmatic for any cell the user moves to. It does not replace the range announcement, because changes on unfocused cells are not spoken.

Permalink to "Related"

← Back to Cell Selection & Clipboard in Data Grids