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.

Virtual cursor mode and forms mode Comparison of JAWS virtual cursor mode and forms mode showing which keys reach the page in each and what that means for a data grid. Virtual cursor mode and forms mode✓ Virtual cursor (browse)Arrow keys move the JAWS cursor, not your focusQuick-nav keys like T and H workThe user reads the page as a documentYour key handlers never see arrows✗ Forms mode (application)Arrow keys reach your handlersQuick-nav keys stop workingThe user interacts with the widgetYou own every navigation key
The mode decides whether your arrow-key handler ever runs.

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.

Choosing a role by the mode it implies Matrix of three table markup choices against the JAWS mode each produces and the keyboard obligations that follow. Choosing a role by the mode it impliesJAWS modeYou must implementPlain table withsortable headersvirtual cursornothing — Tab and Enter are enoughrole="grid"forms mode on entryarrows, Home, End, Ctrl combinations, rovingfocusrole="application"forms mode everywhere insideevery key, including reading commands
Pick the role by how much of the keyboard you are prepared to own.

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
});
What switches JAWS into forms mode Three-part diagram of the triggers that switch JAWS into forms mode: focusing a form control, entering an element with an application-ish role, and the manual Enter key. What switches JAWS into forms modeFocus a form controlinput, select, textareaAutomatic, and expectedEnter a composite rolerole="grid", "treegrid", "listbox", "application"Automatic — every key is now yoursPress Enter manuallythe user forces forms modeAlways available as an escape hatch
A composite role switches the mode automatically — which is a promise you then have to keep.

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.

Permalink to "Related"

Back to Assistive Technology Behaviour Differences