JAWS Virtual Cursor Versus Forms Mode in Data Grids
Permalink to "JAWS Virtual Cursor Versus Forms Mode in Data Grids"Nearly every “my arrow keys do not work in JAWS” report has the same cause, and it is not a bug. JAWS reads pages through a virtual buffer and, in that mode, arrow keys move its own cursor rather than reaching the page. Your handlers never fire because the keystroke never arrives. This page covers the two modes, what switches between them, and the decision that follows: whether your table should take a composite role at all.
It extends assistive technology behaviour differences, and it directly informs the role choice in composite widget roles and states.
Spec reference
Permalink to "Spec reference"The mode distinction is a screen reader implementation, not an ARIA feature, but ARIA is what drives it. JAWS switches into forms mode — sometimes called application mode — automatically when focus enters a form control or an element whose role implies a composite widget: grid, treegrid, listbox, menu, tree, tablist, application.
The consequence is contractual. By taking one of those roles you are telling the reader that your widget handles its own keyboard, and the reader stops offering its own navigation in exchange. If you take role="grid" and do not implement arrow keys, Home, End and the Ctrl combinations, the user is left with a widget that has taken their reading commands away and given nothing back.
NVDA has the same distinction under different names — browse mode and focus mode — and switches on the same triggers, which is why the guidance transfers.
When to use — and when not to
Permalink to "When to use — and when not to"Take a composite role when the widget genuinely is one: a spreadsheet-like editable grid, a listbox, a data grid with cell-level navigation and a roving tabindex.
Do not take one for a table whose interactions are sorting, filtering and following links. Those work perfectly in virtual cursor mode, and the reader’s own table navigation commands — which let a user move down a column comparing values — are worth more than arrow-key handlers.
Do not use role="application" on a data view. It suppresses browse mode across the whole subtree, including any prose inside it, and users cannot read the content at all without leaving the region.
Annotated code example
Permalink to "Annotated code example"<!-- Choice A: a plain table. JAWS stays in virtual cursor mode.
Sorting works with Tab and Enter; table navigation commands still work. -->
<table>
<caption>Outstanding invoices</caption>
<thead>
<tr><th scope="col" aria-sort="ascending">
<button type="button">Amount</button>
</th></tr>
</thead>
<tbody> … </tbody>
</table>
<!-- Choice B: a composite grid. JAWS switches to forms mode on entry.
Now EVERY navigation key is yours to implement. -->
<table role="grid" aria-rowcount="412" aria-colcount="7">
<tbody>
<tr role="row" aria-rowindex="2">
<!-- roving tabindex: exactly one cell is tabbable at a time -->
<td role="gridcell" tabindex="0" aria-colindex="1">Nordics AB</td>
<td role="gridcell" tabindex="-1" aria-colindex="2">412.00</td>
</tr>
</tbody>
</table>
// The obligation that comes with Choice B
grid.addEventListener('keydown', (e) => {
const keys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight',
'Home', 'End', 'PageUp', 'PageDown'];
if (!keys.includes(e.key)) return;
e.preventDefault(); // in forms mode these reach us
moveFocus(e); // and nothing else will move focus for the user
});
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Key | Virtual cursor mode | Forms mode |
|---|---|---|
| Arrow keys | Move the JAWS cursor through the buffer | Reach your keydown handler |
| H, T, K | Jump to next heading, table, link | Type those letters into the widget |
| Ctrl + Alt + arrows | Table navigation, cell by cell | Not available |
| Tab | Moves between focusable elements | Moves between focusable elements |
| Enter | Activates, or forces forms mode | Activates |
Tab behaves identically in both modes, which is why a table that relies only on Tab and Enter is so robust: it works the same way whatever mode the user is in.
Integration context
Permalink to "Integration context"The mode question is the practical form of the role question raised in composite widget roles and states. It also explains a difference documented in NVDA vs JAWS aria-sort announcement differences: a user in virtual cursor mode encounters the sortable header while reading, and hears aria-sort as part of the header; a user in forms mode arrives at it by tabbing, and hears the button first.
If you do take a composite role, the roving tabindex model in implementing roving tabindex for custom data grids is the standard way to meet the obligation.
Gotchas
Permalink to "Gotchas"A composite role with no key handling. The worst of both worlds: reading commands gone, navigation not implemented.
role=“application” on a container of prose. Users cannot read the text inside it with their normal commands.
Testing only with NVDA. NVDA’s automatic focus-mode switching is slightly more forgiving, so a widget can appear to work there and fail in JAWS.
Assuming forms mode is permanent. The user can leave it at any time with the Escape or the plus key, so the widget must still be usable when they do — which mostly means Tab and Enter must be sufficient for the primary actions.
FAQ
Permalink to "FAQ"Why do my arrow-key handlers never fire in JAWS?
Because JAWS is in virtual cursor mode, where arrow keys move its own cursor through a document buffer rather than reaching the page. That is correct behaviour for a document. If your widget genuinely needs arrow keys, give it a composite role such as grid or listbox, which switches JAWS into forms mode automatically — and then implement every navigation key, because the reader’s own ones stop working.
Should a sortable data table use role="grid"?
Usually not. A table whose only interactions are sorting and following links works perfectly in virtual cursor mode: Tab reaches the header buttons, Enter activates them, and the reader’s table navigation commands still work. Adding role=“grid” switches JAWS into forms mode and takes those reading commands away, in exchange for arrow-key navigation you then have to build and maintain.
How do I test which mode JAWS is in?
JAWS plays distinct sounds when it switches, and the mode is reported in the JAWS window. In practice the quickest check is behavioural: press the H key. If it jumps to the next heading you are in virtual cursor mode; if it types the letter H into your widget, you are in forms mode.
Related
Permalink to "Related"- Assistive technology behaviour differences — the parent guide and the triage method
- Composite widget roles and states — the role decision this mode question drives
- NVDA vs JAWS aria-sort announcement differences — where the modes change what is heard