Announcing Cell Context on Navigation
Permalink to "Announcing Cell Context on Navigation"When a screen reader user moves from one cell to the next, they hear some mix of value, column header, row header, position and state. Too little, and they lose track of which record they are in. Too much, and every move takes three seconds. Screen readers already announce changed headers when markup is right; most problems come from missing row headers, aria-label overrides on cells and live regions that duplicate what the screen reader says anyway.
This guide belongs to screen reader announcement strategies.
Spec reference
Permalink to "Spec reference"- SC 1.3.1 (A) — header relationships programmatic (
th,scope,columnheader,rowheader). - ARIA grid —
rowheaderandcolumnheaderroles;aria-describedbyfor extra cell information. - Screen reader behaviour — NVDA and JAWS announce headers that changed since the last cell; VoiceOver announces headers depending on settings.
When to use — and when not to
Permalink to "When to use — and when not to"Use native header semantics for every grid and table. Add descriptions only for important state that is not in the cell text: validation errors, edited-but-unsaved values, computed warnings.
Do not set aria-label on cells to build custom announcements like “Row 3, Amount, 1,240”; it replaces the screen reader’s own context handling and repeats information. The misapplication: a live region that announces “Row 3 column 4” on every arrow key, doubling every announcement.
Annotated code example
Permalink to "Annotated code example"<div role="grid" aria-label="Invoices">
<div role="row">
<span role="columnheader">Invoice</span><span role="columnheader">Status</span><span role="columnheader">Amount</span>
</div>
<div role="row">
<span role="rowheader">INV-1042</span> <!-- row identifier -->
<span role="gridcell">Overdue</span>
<span role="gridcell" aria-describedby="d-1042-amt">1,240.00</span>
<span id="d-1042-amt" hidden>Edited, not saved</span> <!-- extra state only -->
</div>
</div>
<!-- Anti-pattern: overriding the screen reader's context -->
<span role="gridcell" aria-label="Row 3, column Amount, value 1,240.00">1,240.00</span>
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Movement | NVDA | JAWS | VoiceOver |
|---|---|---|---|
| Right, same row | “Amount, 1,240.00” | “Amount, 1,240.00” | “1,240.00, Amount” |
| Down, same column | “INV-1043, 2,980.00” | “INV-1043, 2,980.00” | “2,980.00” (+ row header setting) |
| Into a cell with description | Value, then “Edited, not saved” | Same | Description after pause |
| With aria-label override | Label replaces context | Same | Same |
Step-by-step implementation
Permalink to "Step-by-step implementation"- Identify the row header. Choose the column that identifies a record (invoice number, name) and make it a
th scope="row"orrole="rowheader". - Keep column headers simple. Short header text makes repeated announcements quicker; put help in descriptions on the header, not in its name.
- Remove overrides. Delete
aria-labelfrom cells unless the visible text is genuinely unhelpful (an icon), and then label only the content. - Add descriptions for state. Use
aria-describedbyfor errors, unsaved edits or warnings; keep them short. - Listen and compare. Navigate the same path in NVDA, JAWS and VoiceOver, and adjust markup rather than adding announcements.
Integration context
Permalink to "Integration context"Header markup is covered in correct usage of scope and headers in complex tables and multi-level headers in multi-level column headers with colspan. Positions in partial grids are handled by aria-rowindex and aria-colindex in practice. Column help descriptions follow aria-describedby for column help text. VoiceOver specifics are in VoiceOver strategies for announcing table updates.
Gotchas
Permalink to "Gotchas"aria-activedescendant grids. With active descendant focus management, screen readers rely on the referenced cell’s role and headers; make sure the referenced element is the cell, not an inner span.
Interactive content in cells. A focusable button inside a cell is announced by its own name; include the row identifier in that name.
Long headers. A forty-character header repeated on every column move is tiring; abbreviate visibly or with abbr on th.
Edge cases
Permalink to "Edge cases"- Merged cells. Spanning cells announce headers for their first row and column; check that this matches what users expect.
- Empty cells. Screen readers say “blank”; if empty means something (“not applicable”), put that text in the cell, visually hidden if necessary.
- Icons as values. A status icon needs text: “Overdue”, not “image”.
Worked example
Permalink to "Worked example"A CRM grid set aria-label on every cell to “Row N, Column Name, Value” and also announced moves in a live region. NVDA users heard each cell twice, with the position first, so moving across a row took several seconds per cell. The team removed the labels and the live region, made the customer name a row header, and added descriptions only for unsaved edits. Announcements shrank to “Phone, 555 0142” and users navigated twice as fast.
Common mistakes
Permalink to "Common mistakes"- aria-label on every cell.
- No row headers.
- Live regions for navigation.
- Status shown only by colour.
Testing checklist
Permalink to "Testing checklist"Testing it with a screen reader
Permalink to "Testing it with a screen reader"In NVDA, move right across a row and listen: you should hear the column header and value each time, and nothing about the row. Move down a column and listen for the row header and value. Turn on the speech viewer to compare announcement lengths before and after changes. Repeat in JAWS with its table navigation keys, and in VoiceOver, where you may need to enable “Speak headers” in table settings. Note any differences in your screen reader test matrix.
Measuring announcement length
Permalink to "Measuring announcement length"A simple metric helps teams keep announcements short: words spoken per cell move. Use NVDA’s speech viewer or a Guidepup script to capture the spoken text for a fixed path through the grid — say twenty moves — and count words. A good target for column moves is under six words, including header and value. Track the number in your test suite so a future change that adds labels or descriptions to every cell shows up as a regression.
Choosing the row header column
Permalink to "Choosing the row header column"The best row header is short, unique and meaningful to users: an invoice number, a person’s name, a product code. Avoid long free-text columns such as descriptions, which make every vertical move slow. If no single column identifies a row, combine two in a visually hidden row header (“Lund & Co, March”), or reorder columns so the identifying column comes first. If the visual design puts a checkbox first, the checkbox cell can stay first while the identifying column is still marked as the row header.
Design system notes
Permalink to "Design system notes"Have the grid component require a row header column and forbid cell-level aria-label except for icon content, with lint checks.
FAQ
Permalink to "FAQ"Should I announce row and column numbers?
Screen readers can report them on request; do not add them to every cell.
Why do I hear headers twice?
Probably an aria-label or live region duplicating native context.
Can I customise what is read?
Through markup: headers, row headers and descriptions. Avoid overrides.
What about VoiceOver?
It reads headers depending on user settings; ensure semantics are correct and test.
When is aria-label on a cell acceptable?
For icon-only content, and then only to name the icon.
Do descriptions slow navigation?
Slightly; use them only for state that matters.
Related
Permalink to "Related"- aria-rowindex and aria-colindex in practice — positions
- VoiceOver strategies for announcing table updates — VoiceOver specifics
- Verbosity settings for data announcements — user control
← Back to Screen Reader Announcement Strategies for Data Interfaces