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.
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 |
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.
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.
Related
Permalink to "Related"- Extending selection with Shift and arrows — the keys that create the range
- aria-selected on cells versus rows — the state behind the message
- Debouncing status messages — the settle timing used here