[Skip navigation links]
Login

Layout table vs data table detection

Posted by Mark Rogers on Feb 21, 2016 | 

Accessibility

Summary

A common mantra is ‘don’t use layout tables’ but the reality is the screen reader decides which tables are layout tables, and this doesn’t always match the author’s intent.

This post describes how screen readers decide which HTML table elements are layout tables, and which are data tables. Although the author can try to influence this decision, ultimately it’s the assistive technology that decides.

Background

In the early days of the the web, tables were commonly used for layout due to the widely varying CSS support in different browsers. This is less common now, but sometimes still happens. This technique presented problems for screen reader users because tabular relationships were voiced for non-tabular content. For example, a navigation bar in one cell and content in another cell would be voiced as a table row.

Screen readers compensate for this using heuristics to guess if a table is used for layout. When a layout table is detected, a screen reader linearizes table cells into a series of paragraphs, which prevents the table data being voiced as a rows and columns.

Unfortunately, this causes serious problems when a data table is wrongly identified as a layout table. For example, consider trying to understand the Periodic Table of the Elements as a long series of element names without the columns.

Layout table detection heuristics
Element or @attribute NVDA
IE
NVDA
Firefox
JAWS
IE
JAWS
Firefox
VoiceOver
Safari
HTML 5.0
th Data Data Data (1) Data (1) Data (2) Data
@role=presentation Layout Layout Layout Layout Layout Layout
thead Data Data N/A N/A Data Data
tfoot Data Data N/A N/A Data N/A
caption Data Data (3) N/A N/A Data Data
col N/A Data N/A N/A Data N/A
colgroup Data Data N/A N/A Data N/A
rowgroup Data N/A N/A N/A N/A N/A
@contenteditable N/A Data N/A N/A Data N/A
@summary="" N/A N/A N/A N/A N/A N/A
@summary="non-empty" Data Data N/A N/A Data N/A
@border=0 ? ? ? ? ? Layout
@border=1 ? ? ? ? ? Data
@rules ? ? ? ? Data N/A
@headers Data Data (6) N/A N/A ? Data
@scope N/A Data (6) N/A N/A N/A Data
@axis N/A Data (6) N/A N/A N/A N/A
@abbr N/A Data (6) N/A N/A N/A N/A
@datatable=0 N/A Layout Layout Layout N/A Non-standard attribute
@datatable=1 N/A N/A Data Data N/A Non-standard attribute
@datatable=true N/A N/A Data Data N/A Non-standard attribute
CSS borders N/A Data N/A N/A Data (4) Data
CSS cell background color N/A N/A N/A N/A Data (5) N/A
CSS alternating row colors N/A Data N/A N/A Data N/A
Contains form controls N/A N/A N/A N/A N/A N/A
Contains embed, applet or iframe N/A Layout N/A N/A N/A N/A
Contains nested table N/A Layout N/A N/A N/A N/A
Only 1 cell N/A Layout N/A N/A Layout N/A
Only 1 row N/A Layout N/A N/A N/A N/A
Only 1 column N/A Layout N/A N/A N/A N/A
5 or more columns N/A Data N/A N/A N/A N/A
20 or more rows N/A Data N/A N/A Data N/A
2-4 columns, no borders, and >= 95% of doc width N/A Layout N/A N/A N/A N/A
2-4 columns, no borders, and 10 or fewer cells N/A Layout N/A N/A N/A N/A
JAWS Heisenberg heuristic N/A N/A Data (8) Data (8) N/A N/A
Default Layout (9) Data Layout (9) Layout (9) Layout (9) N/A

Notes

This table shows the main interoperability issues, but is a simplification! It doesn’t capture all of the implementation subtleties in edge cases.

(1) From JAWS 11.0.756 onwards.

(2) Only if TH is in first column or first row. VoiceOver ignores other THs for the purpose of layout role calculation.

(3) Firefox ignores caption for layout role calculation if it’s not first child, is empty or has an ARIA role.

(4) Only if at least 50% of the cells have borders.

(5) Only if at least 50% of the cells have a different background color to the table background color.

(6) Only if attribute is non-empty.

(7) This prevents spreadsheet-style data entry tables.

(8) If a table doesn’t have TH elements or role=presentation then JAWS uses the following astonishing heuristic:

(9) Fallback to a data table which announces table structure is a much better fallback, since removing table structure when it’s needed causes major problems.

The JAWS Heisenberg heuristic

If table doesn’t have TH elements or role=presentation the following astonishing heuristic is used:

This is affected by default font size and by window size if table is size relative to viewport width (e.g. width=100%). Changing the default font size, or resizing the window can change the table from a layout table to a data table, or vice versa.

The calculation is also inconsistent between IE (where cell margins are included in the square pixel calculation) and Firefox (where cell margins are excluded). An algorithm connected to a Gieger counter controlling the fate of a cat in a box is nearly as predictable.

Reference: Tables and Forms with JAWS and MAGic

What the specs say

The HTML 5.0 spec has suggestions for implementing heuristics (shown in the last column of the table above) but cautions «It is quite possible that the … suggestions are wrong.»

https://html.spec.whatwg.org/multipage/tables.html#the-table-element

How the implementations actually work

NVDA with Firefox

NVDA with Firefox uses the IAccessible2 API, so the role and name calculation is done by Firefox and exposed through IAccessible2. The type of table is exposed through a non-standard attribute called ‘layout-guess’. This attribute is used to tell NVDA whether to ignore the table because it’s a layout table. This can be over-ridden by the ‘Include layout tables’ setting in NVDA.

  1. TH or TD with SCOPE=COL or SCOPE=COLGROUP is a column header
  2. TH or TD with SCOPE=ROW or SCOPE=ROWGROUP is a row header
  3. TH with TD to the right is a row header
  4. TH with TD below is a column header
  5. TH not matching any of the above : guess based on TH ROWSPAN attribute
  6. TD without scope is a cell

Header calculation:

  1. If TD HEADERs list specified
    • Add all items in list with role = column header role to TD column headers
    • Add all items in list with in same column with role != role header to TD column headers
    • Add all items in list with role = row header role to TD row headers
    • Add all items in list with in same row with role != column header to TD row headers
  2. If no TD HEADERs
    • Add all cells above TD with role = column header to TD column headers
    • Add all cells to left of TD with role = row header to TD row headers

The code is in TableAccessible::IsProbablyLayoutTable in accessible/generic/TableAccessible.cpp.

Tags:

First posted Feb 2016