VoiceOver on iOS: the Rotor and Data Tables

Permalink to "VoiceOver on iOS: the Rotor and Data Tables"

VoiceOver on iPhone and iPad reads the web through touch: users swipe right and left to move to the next and previous element, and use the rotor — a two-finger twist gesture — to choose what swipes up and down move by: headings, links, form controls, or, inside a table, rows and columns. When a data table is marked up well, iOS VoiceOver users can move cell by cell with headers announced, jump a row or column at a time, and hear “row 3 of 12, column 2 of 5”. When it is not — or when responsive CSS has stripped its semantics — the same table is a flat stream of numbers.

This page describes iOS VoiceOver’s table behaviour and the markup and testing that make it work. It belongs to assistive technology behaviour differences.

Spec reference

Permalink to "Spec reference"

iOS VoiceOver reads the accessibility tree built by WebKit from the same HTML and ARIA semantics as desktop browsers. Relevant behaviour:

  • Native <table> elements with <th> headers are exposed as tables; the rotor gains Rows and Columns options (in current iOS versions, “Rows” and “Columns” appear in the rotor inside tables; older versions show them as “Vertical Navigation”).
  • Header cells are announced when the user moves into a new column (column header) or row (row header), subject to the Verbosity setting “Table Headers”.
  • role="grid" is exposed, but VoiceOver on iOS does not provide the grid’s arrow-key model to touch users; they swipe through cells as in a table.
  • WebKit may drop table semantics when table elements have non-table CSS display values; explicit ARIA table roles restore them.

Criteria: SC 1.3.1 Info and Relationships (header relationships must survive to iOS), SC 1.3.2 Meaningful Sequence (swipe order follows DOM order), SC 2.5.1–2.5.4 pointer criteria apply to any custom gestures you add.

Reading a table with the iOS rotor Steps for reading a data table with VoiceOver on iOS: swipe into the table, set the rotor to Rows or Columns, swipe up and down to move, and swipe right and left to move by cell. Reading a table with the iOS rotorSwipe into the table"Open invoices, table, 12 rows, 4 columns"caption read firstTwist the rotorselect Rows or Columnsonly offered inside tablesSwipe down / upnext or previous row (or column)header announcedSwipe right / leftnext or previous cell"Amount, 1,280.00"
The rotor turns vertical swipes into row or column jumps — the touch equivalent of table navigation keys.

When iOS behaviour matters — and how much to design for it

Permalink to "When iOS behaviour matters — and how much to design for it"

It matters for any data product used on phones and tablets: field-service apps, mobile dashboards, approvals on the go. iOS VoiceOver is the most used mobile screen reader in many markets.

You rarely need iOS-specific code. Native table markup, working header associations, and responsive CSS that does not strip semantics cover nearly everything. The work is in testing on a device, because desktop Safari with VoiceOver and iOS VoiceOver differ in gesture model and in some rendering paths.

The misapplication to name is testing responsive table layouts only in a desktop browser resized to phone width. The card layout reads correctly in desktop Chrome and loses its table semantics in iOS Safari, exactly where it is used.

Annotated code example

Permalink to "Annotated code example"
<!-- Native semantics iOS can expose as a navigable table -->
<table>
  <caption>Open invoices</caption>                        <!-- read on entry -->
  <thead>
    <tr>
      <th scope="col">Invoice</th>
      <th scope="col">Customer</th>
      <th scope="col">Due</th>
      <th scope="col">Amount (EUR)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">INV-1042</th>                       <!-- row header on row changes -->
      <td>Northwind</td><td>2026-03-04</td><td>1,280.00</td>
    </tr>
  </tbody>
</table>
/* If you reflow on phones, restore roles explicitly (see stacked card guide) */
@media (max-width: 40rem) {
  table.stack, table.stack tr, table.stack td { display: block; }
}
<!-- …and in the markup: explicit roles keep WebKit from flattening the table -->
<table class="stack" role="table">
  <tr role="row"><th role="rowheader" scope="row">INV-1042</th>
    <td role="cell" data-label="Customer">Northwind</td></tr>
</table>

For wide tables in horizontal scroll containers, iOS VoiceOver scrolls the container automatically as users swipe to off-screen cells — one of the few places iOS is more forgiving than desktop. The container still needs a name if it is focusable.

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Gesture Expected result Deviation to watch
Swipe into table “Open invoices, table, 12 rows, 4 columns” Without a caption: “table” only
Rotor → Rows, swipe down Next row, row header read Missing if the row has no th
Rotor → Columns, swipe right Next column, column header read Headers skipped if Verbosity → Table Headers is off
Swipe right through cells Cell values with headers on column change Flat values if table semantics were stripped
Double-tap a sort button in a header Activates; status message read Message dropped if fired during a scroll
Explore by touch Touched cell read with headers —
Table markup and what iOS VoiceOver provides Matrix of four table implementations and whether iOS VoiceOver provides table entry announcement, rotor rows and columns, and header announcements for each. Table markup and what iOS VoiceOver providesImplementationTable announcedRotor rows/columnsHeaders readNative table with thYesYesYesReflowed, explicit rolesYesYesYesReflowed, no rolesOften noNoNodiv role="grid"YesVariesOnly with columnheaderroles
Native tables and explicitly-roled reflowed tables work; CSS-reflowed tables without roles and div grids do not.

Integration context

Permalink to "Integration context"

The Android counterpart, with its own gesture and reading-control model, is in how TalkBack differs for data grid navigation. The semantics trap for reflowed tables and the explicit-roles fix are detailed in stacked card layouts that keep table semantics.

Desktop VoiceOver with Safari shares the WebKit accessibility tree, so a desktop smoke test catches semantic regressions cheaply — see VoiceOver smoke tests with guidepup — but gestures, the rotor and scrolling need a real device.

Desktop VoiceOver versus iOS VoiceOver for tables Comparison of how desktop VoiceOver with Safari and iOS VoiceOver navigate data tables. Desktop VoiceOver versus iOS VoiceOver for tablesDesktop VoiceOver + SafariVO + arrow keys move by cellTable commands via the VO modifierRotor via VO+U lists tablesAutomatable with guidepupiOS VoiceOverSwipe left and right move by elementRotor twist selects Rows or ColumnsExplore by touch reads the touched cellManual testing on a device
Same accessibility tree, different interaction model — test both before a mobile release.

Gotchas

Permalink to "Gotchas"

Verbosity settings. Table header announcement can be turned off by users. Test with the default, and do not put information only in headers that users might mute.

Sticky headers. Sticky <thead> works; cloned header tables appear twice in swipe order.

Custom swipe gestures. Touch-and-drag interactions in the page conflict with VoiceOver gestures. Any gesture-driven feature needs a button alternative (SC 2.5.1).

Design system notes

Permalink to "Design system notes"

Include iOS VoiceOver on a real device in the component library’s release checklist for table, grid and card-layout components. Record the exact announcement strings for a reference table so regressions are easy to spot by ear.

Testing checklist

Permalink to "Testing checklist"

FAQ

Permalink to "FAQ"
How do VoiceOver users on iPhone navigate a data table?

They swipe right and left to move cell by cell, and use the rotor to switch vertical swipes to Rows or Columns, which moves a whole row or column at a time. Headers are announced when moving into a new row or column.

Why does my responsive table read as a flat list on iOS?

Because changing table elements’ CSS display values can make WebKit drop their table semantics. Add explicit ARIA table roles to the table, rows, headers and cells.

Can iOS VoiceOver testing be automated?

Not in the same way as desktop. Desktop VoiceOver with Safari shares the WebKit accessibility tree and can be automated with guidepup, which catches many semantic regressions, but gestures and the rotor need testing on a device.

Does role="grid" help on iOS?

Not much. iOS VoiceOver does not offer the grid’s arrow-key model to touch users. Native tables with good headers work best on iOS.

Permalink to "Related"

← Back to Assistive Technology Behaviour Differences