Shift-Click Range Selection With Keyboard Equivalents
Permalink to "Shift-Click Range Selection With Keyboard Equivalents"Shift-click is the fastest way to select a run of rows and the easiest interaction in a data grid to leave mouse-only. This page covers the keyboard equivalent — an explicit anchor plus Shift-modified movement — and the announcement discipline that keeps a twenty-row range from producing twenty announcements.
It is the range detail beneath bulk selection and batch actions, and its announcements follow announcing selection count changes in data grids.
Spec reference
Permalink to "Spec reference"SC 2.1.1 Keyboard (Level A) is the governing criterion: every function must be operable through a keyboard interface. Range selection is a function, and shift-click is a pointer gesture, so the keyboard path is mandatory rather than a nicety.
SC 4.1.3 Status Messages (Level AA) covers the announcement. A selection change does not move focus, so the count must reach assistive technology through a status region.
The ARIA involved is minimal. Each row carries aria-selected when the grid uses role="grid" with aria-multiselectable="true"; in a plain table with checkbox cells, the checkbox state is the selection state and no extra ARIA is needed. There is no attribute for the selection anchor — it is application state, exposed through behaviour and announcements.
When to use — and when not to
Permalink to "When to use — and when not to"Implement range selection wherever users select more than a handful of rows: bulk approval queues, export selections, batch assignment. The keyboard path is required regardless of how the pointer path is built.
Do not build range selection into a grid where only one row can be selected at a time. A single-select grid uses aria-selected on one row and needs no anchor.
Do not overload Shift with a second meaning in the same grid. If Shift plus Enter adds a sort key, as in multi-column sort announcement patterns, keep Shift plus arrows for selection and Shift plus Enter for sorting — the contexts differ, but the combination should be documented in the shortcut help.
Annotated code example
Permalink to "Annotated code example"// SC 2.1.1: the keyboard equivalent of shift-click
let anchor = null; // row index where the range starts
let selected = new Set();
let announceTimer = null;
function onRowKeydown(e, index) {
if (e.key === ' ') {
e.preventDefault(); // Space must not scroll the grid
anchor = index; // a plain Space (re)sets the anchor
toggleRow(index);
scheduleAnnounce();
return;
}
const extend = e.shiftKey;
let next = index;
if (e.key === 'ArrowDown') next = Math.min(index + 1, lastIndex);
else if (e.key === 'ArrowUp') next = Math.max(index - 1, 0);
else if (e.key === 'Home') next = 0;
else if (e.key === 'End') next = lastIndex;
else return;
e.preventDefault();
focusRow(next);
if (extend) {
if (anchor === null) anchor = index;
selectRange(anchor, next); // recomputed from the anchor, not accumulated
scheduleAnnounce();
}
}
function selectRange(from, to) {
const [lo, hi] = from <= to ? [from, to] : [to, from];
selected = new Set(); // recompute so shrinking the range works
for (let i = lo; i <= hi; i++) selected.add(rowIdAt(i));
syncCheckboxes();
}
// SC 4.1.3: one announcement per settled gesture, not one per row
function scheduleAnnounce() {
clearTimeout(announceTimer);
announceTimer = setTimeout(() => {
announce(`${selected.size} of ${totalRows} rows selected`);
}, 200);
}
Recomputing the range from the anchor rather than accumulating is what makes shrinking work. A user who overshoots and presses Shift plus Up expects the range to contract; an implementation that adds rows on every extend leaves the overshoot selected, which is very confusing when you cannot see the highlight.
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Assistive technology | On extend | Deviation |
|---|---|---|
| NVDA + Firefox | Reads each focused row, then the debounced summary | Row reading can lag a fast extend |
| JAWS + Chrome | Reads each focused row and its selected state | Announces aria-selected per row, which can be verbose |
| VoiceOver + Safari | Reads the focused row; the summary follows | Drops the summary if it arrives during row speech |
| TalkBack + Chrome | Not applicable for keyboard extend | Provide a “select range” menu action for touch |
Integration context
Permalink to "Integration context"The selection total and the header checkbox state are derived from the same set, which is what implementing select-all with indeterminate checkbox state describes. A range selection that leaves the header checkbox unsynchronised is a state the user can hear as contradictory.
Selection must also survive sorting and filtering, or it must be cleared explicitly. Selecting a range, sorting the table, and finding that the selection now covers a different set of rows is a serious data-integrity risk in a bulk-delete flow. Key the selection set by row id, and if a filter removes selected rows, announce it: “3 selected rows are no longer visible.”
Gotchas
Permalink to "Gotchas"Space scrolls the page. Always preventDefault on Space inside the grid.
The anchor is never reset. Without a reset on plain selection, ranges grow from wherever the user first pressed Space, which becomes unpredictable after a few interactions.
Per-row announcements. Covered above — debounce.
Range selection across pages. A range that starts on page 1 and extends after a page change is almost never what the user meant. Reset the anchor on a page change and announce the selection scope.
FAQ
Permalink to "FAQ"What is the keyboard equivalent of shift-click range selection?
An explicit anchor plus Shift-modified movement. A plain Space sets the anchor and toggles that row; Shift with the arrow, Home or End keys extends the selection from the anchor to the current row. This mirrors what shift-click does with a pointer, and it is the model users already know from file managers and spreadsheets.
How should the range be announced?
Once, when the gesture settles, describing the change and the new total: “8 rows added, 11 of 587 selected.” Debounce by a couple of hundred milliseconds so that holding Shift and Down produces one announcement rather than one per row. Per-row announcements make fast range selection impossible, because the speech queue never catches up.
Does the anchor need to be exposed to assistive technology?
Not as an attribute — there is no ARIA for it. It is exposed through behaviour and through the announcement: naming the span “rows 12 to 19” tells the user where the range started. If the anchor row is also visually marked, keep that marker decorative and let the announcement carry the meaning.
Related
Permalink to "Related"- Bulk selection & batch actions — the parent guide and the selection model
- Announcing selection count changes in data grids — the debounced message this reuses
- Implementing select-all with indeterminate checkbox state — the header state derived from the same set